diff --git a/.gitattributes b/.gitattributes index d321e2a910..2e8a45ca5c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,9 @@ mobile/openapi/**/*.dart linguist-generated=true mobile/lib/**/*.g.dart -diff -merge mobile/lib/**/*.g.dart linguist-generated=true +mobile/lib/**/*.drift.dart -diff -merge +mobile/lib/**/*.drift.dart linguist-generated=true + open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts linguist-generated=true diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 69a0a8f611..887009eb4c 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 7ae1b4ea11..5a48d0c323 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -42,7 +42,7 @@ jobs: token: ${{ steps.generate-token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5 + uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 - name: Bump version run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" @@ -70,7 +70,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7b6310667..024ed15fa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -384,7 +384,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Install uv - uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5 + uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # with: @@ -525,7 +525,7 @@ jobs: - name: Generate new migrations continue-on-error: true - run: npm run typeorm:migrations:generate ./src/migrations/TestMigration + run: npm run migrations:generate TestMigration - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20 @@ -538,7 +538,7 @@ jobs: run: | echo "ERROR: Generated migration files not up to date!" echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" - cat ./src/migrations/*-TestMigration.ts + cat ./src/*-TestMigration.ts exit 1 - name: Run SQL generation diff --git a/.vscode/settings.json b/.vscode/settings.json index 49dbf3944c..49692809bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,7 @@ ], "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { - "*.ts": "${capture}.spec.ts,${capture}.mock.ts" + "*.ts": "${capture}.spec.ts,${capture}.mock.ts", + "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart" } } \ No newline at end of file diff --git a/README.md b/README.md index fea9801d41..19886f9873 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ For the mobile app, you can use `https://demo.immich.app` for the `Server Endpoi | Read-only gallery | Yes | Yes | | Stacked Photos | Yes | Yes | | Tags | No | Yes | -| Folder View | No | Yes | +| Folder View | Yes | Yes | ## Translations diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs index 9115a1feb7..101d91bea4 100644 --- a/cli/eslint.config.mjs +++ b/cli/eslint.config.mjs @@ -1,39 +1,29 @@ -import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import tsParser from '@typescript-eslint/parser'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import typescriptEslint from 'typescript-eslint'; 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 [ +export default typescriptEslint.config([ + eslintPluginUnicorn.configs.recommended, + eslintPluginPrettierRecommended, + js.configs.recommended, + typescriptEslint.configs.recommended, { ignores: ['eslint.config.mjs', 'dist'], }, - ...compat.extends( - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'plugin:unicorn/recommended', - ), { - plugins: { - '@typescript-eslint': typescriptEslint, - }, - languageOptions: { globals: { ...globals.node, }, - parser: tsParser, + parser: typescriptEslint.parser, ecmaVersion: 5, sourceType: 'module', @@ -58,4 +48,4 @@ export default [ 'object-shorthand': ['error', 'always'], }, }, -]; +]); diff --git a/cli/package-lock.json b/cli/package-lock.json index 96b53baa44..84b61991d4 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.57", + "version": "2.2.61", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.57", + "version": "2.2.61", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -27,9 +27,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.13.10", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", + "@types/node": "^22.13.14", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -37,12 +35,13 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", + "typescript-eslint": "^8.28.0", "vite": "^6.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", @@ -55,14 +54,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.130.3", + "version": "1.131.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "typescript": "^5.3.3" } }, @@ -92,89 +91,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -195,91 +125,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", @@ -746,16 +591,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.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "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" } @@ -1163,10 +1012,11 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -1512,9 +1362,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.13.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz", + "integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==", "dev": true, "license": "MIT", "dependencies": { @@ -1525,20 +1375,21 @@ "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/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1558,16 +1409,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "engines": { @@ -1583,14 +1434,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1601,14 +1452,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1625,9 +1476,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "license": "MIT", "engines": { @@ -1639,14 +1490,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1666,16 +1517,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1690,13 +1541,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1721,9 +1572,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", - "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", + "integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1736,7 +1587,7 @@ "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, @@ -1744,8 +1595,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.9", - "vitest": "3.0.9" + "@vitest/browser": "3.1.1", + "vitest": "3.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1754,14 +1605,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1770,13 +1621,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1797,9 +1648,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "dev": true, "license": "MIT", "dependencies": { @@ -1810,13 +1661,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.1.1", "pathe": "^2.0.3" }, "funding": { @@ -1824,13 +1675,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1839,9 +1690,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1852,13 +1703,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -1974,9 +1825,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": [ { @@ -2007,12 +1858,13 @@ } }, "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" @@ -2056,9 +1908,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "dev": true, "funding": [ { @@ -2135,9 +1987,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.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", "dev": true, "funding": [ { @@ -2145,6 +1997,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -2218,13 +2071,13 @@ "license": "MIT" }, "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", @@ -2287,9 +2140,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.128", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", + "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "dev": true, "license": "ISC" }, @@ -2299,15 +2152,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "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", @@ -2453,14 +2297,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2471,7 +2315,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -2484,28 +2328,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" @@ -2514,7 +2358,7 @@ "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/globals": { @@ -2682,9 +2526,9 @@ } }, "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2793,6 +2637,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", @@ -2845,15 +2702,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2920,24 +2768,19 @@ "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/hosted-git-info": { + "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": { - "function-bind": "^1.1.2" + "lru-cache": "^10.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^16.14.0 || >=18.0.0" } }, - "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 - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2979,45 +2822,45 @@ } }, "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "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": ">=8" - } - }, - "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-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==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/index-to-position": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.0.0.tgz", + "integrity": "sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA==", "dev": true, - "dependencies": { - "hasown": "^2.0.0" + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-builtin-module": { + "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": "^4.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-extglob": { @@ -3132,7 +2975,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -3147,10 +2991,11 @@ } }, "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==", + "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" }, @@ -3165,12 +3010,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", @@ -3206,12 +3045,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3314,6 +3147,7 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3393,24 +3227,18 @@ "license": "MIT" }, "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/optionator": { @@ -3460,15 +3288,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "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, - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -3488,18 +3307,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.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.2.0.tgz", + "integrity": "sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ==", "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.26.2", + "index-to-position": "^1.0.0", + "type-fest": "^4.37.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3523,12 +3342,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -3700,108 +3513,44 @@ } ] }, + "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/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -3825,41 +3574,29 @@ } }, "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" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, "node_modules/resolve-from": { @@ -3942,9 +3679,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==", "dev": true, "license": "ISC", "bin": { @@ -4008,6 +3745,7 @@ "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" @@ -4017,23 +3755,26 @@ "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 + "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.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "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/stackback": { "version": "0.0.2", @@ -4042,9 +3783,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", "dev": true, "license": "MIT" }, @@ -4103,15 +3844,19 @@ } }, "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "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.0" + "min-indent": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -4138,27 +3883,15 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4271,10 +4004,11 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -4288,6 +4022,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -4302,6 +4049,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -4309,10 +4079,23 @@ "dev": true, "license": "MIT" }, + "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/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -4331,7 +4114,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -4354,15 +4137,16 @@ "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.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "dev": true, "license": "MIT", "dependencies": { @@ -4432,9 +4216,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "dev": true, "license": "MIT", "dependencies": { @@ -4475,31 +4259,31 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "debug": "^4.4.0", - "expect-type": "^1.1.0", + "expect-type": "^1.2.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "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.9", + "vite-node": "3.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4515,8 +4299,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", "happy-dom": "*", "jsdom": "*" }, @@ -4686,9 +4470,9 @@ } }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "dev": true, "license": "ISC", "bin": { diff --git a/cli/package.json b/cli/package.json index 63c55ed790..da6ad41ed3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.57", + "version": "2.2.61", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -21,9 +21,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.13.10", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", + "@types/node": "^22.13.14", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -31,12 +29,13 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", + "typescript-eslint": "^8.28.0", "vite": "^6.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", diff --git a/docs/docs/features/img/moblie-smart-serach.webp b/docs/docs/features/img/mobile-smart-search.webp similarity index 100% rename from docs/docs/features/img/moblie-smart-serach.webp rename to docs/docs/features/img/mobile-smart-search.webp diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md index 15f83949f2..0ee1d01000 100644 --- a/docs/docs/features/searching.md +++ b/docs/docs/features/searching.md @@ -45,7 +45,7 @@ Some search examples: - + @@ -56,7 +56,20 @@ Navigating to `Administration > Settings > Machine Learning Settings > Smart Sea ### CLIP models -More powerful models can be used for more accurate search results, but are slower and can require more server resources. Check the dropdowns below to see how they compare in memory usage, speed and quality by language. +The default search model is fast, but there are many other options that can provide better search results. The tradeoff of using these models is that they're slower and/or use more memory (both when indexing images with background Smart Search jobs and when searching). + +The first step of choosing the right model for you is to know which languages your users will search in. + +If your users will only search in English, then the [CLIP][huggingface-clip] section is the first place to look. This is a curated list of the models that generally perform the best for their size class. The models here are ordered from higher to lower quality. This means that the top models will generally rank the most relevant results higher and have a higher capacity to understand descriptive, detailed, and/or niche queries. The models are also generally ordered from larger to smaller, so consider the impact on memory usage, job processing and search speed when deciding on one. The smaller models in this list are not too different in quality and many times faster. + +[Multilingual models][huggingface-multilingual-clip] are also available so users can search in their native language. Use these models if you expect non-English searches to be common. They can be separated into two search patterns: + +- `nllb` models expect the search query to be in the language specified in the user settings +- `xlm` and `siglip2` models understand search text regardless of the current language setting + +`nllb` models tend to perform the best and are recommended when users primarily searches in their native, non-English language. `xlm` and `siglip2` models are more flexible and are recommended for mixed language search, where the same user might search in different languages at different times. + +For more details, check the tables below to see how they compare in memory usage, speed and quality by language. Once you've chosen a model, follow these steps: diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index f5d2680658..9806af760a 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 100 +--- + # Config File A config file can be provided as an alternative to the UI configuration. diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 99a29397fa..06154161f4 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -69,39 +69,4 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc ## Next Steps -Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below. - -### Setting up optional features - -- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich -- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding -- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich - -### Upgrading - -:::danger Read the release notes -Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower]. - -You can see versions that had breaking changes [here][breaking]. -::: - -If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version. - -When a new version of Immich is [released][releases], the application can be upgraded and restarted with the following commands, run in the directory with the `docker-compose.yml` file: - -```bash title="Upgrade and restart Immich" -docker compose pull && docker compose up -d -``` - -To clean up disk space, the old version's obsolete container images can be deleted with the following command: - -```bash title="Clean up unused Docker images" -docker image prune -``` - -[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml -[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env -[watchtower]: https://containrrr.dev/watchtower/ -[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created -[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry -[releases]: https://github.com/immich-app/immich/releases +Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md). diff --git a/docs/docs/install/post-install.mdx b/docs/docs/install/post-install.mdx index 45b5f446fd..636274aaea 100644 --- a/docs/docs/install/post-install.mdx +++ b/docs/docs/install/post-install.mdx @@ -41,3 +41,9 @@ A list of common steps to take after installing Immich include: ## Step 7 - Setup Server Backups + +## Setting up optional features + +- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich +- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding +- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich diff --git a/docs/docs/install/synology.md b/docs/docs/install/synology.md index cab80df999..0ff6a3b85c 100644 --- a/docs/docs/install/synology.md +++ b/docs/docs/install/synology.md @@ -67,10 +67,4 @@ Click "**Edit Rules**" and add the following firewall rules: ## Next Steps -Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below. - -### Setting up optional features - -- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich -- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding -- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich +Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md). diff --git a/docs/docs/install/truenas.md b/docs/docs/install/truenas.md index 31b007a47d..3cd772de63 100644 --- a/docs/docs/install/truenas.md +++ b/docs/docs/install/truenas.md @@ -247,6 +247,10 @@ Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ` ## Updating the App +:::danger +Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). +::: + When updates become available, SCALE alerts and provides easy updates. To update the app to the latest version: diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 731f53bb00..344b912aea 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -131,6 +131,10 @@ For more information on how to use the application once installed, please refer ## Updating Steps +:::danger +Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). +::: + Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager. =6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -232,97 +161,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", @@ -364,9 +202,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", - "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -381,9 +219,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", - "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -398,9 +236,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", - "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -415,9 +253,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", - "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -432,9 +270,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", - "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -449,9 +287,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", - "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -466,9 +304,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", - "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -483,9 +321,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", - "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -500,9 +338,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", - "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -517,9 +355,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", - "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -534,9 +372,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", - "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -551,9 +389,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", - "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -568,9 +406,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", - "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -585,9 +423,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", - "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -602,9 +440,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", - "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -619,9 +457,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", - "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -636,9 +474,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", - "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -653,9 +491,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", - "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -670,9 +508,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", - "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -687,9 +525,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", - "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -704,9 +542,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", - "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -721,9 +559,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", - "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -738,9 +576,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", - "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -755,9 +593,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", - "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -772,9 +610,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", - "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -789,16 +627,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.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "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" } @@ -1246,10 +1088,11 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -1274,9 +1117,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", - "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.38.0.tgz", + "integrity": "sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==", "cpu": [ "arm" ], @@ -1288,9 +1131,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", - "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.38.0.tgz", + "integrity": "sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==", "cpu": [ "arm64" ], @@ -1302,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", - "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.38.0.tgz", + "integrity": "sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==", "cpu": [ "arm64" ], @@ -1316,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", - "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.38.0.tgz", + "integrity": "sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==", "cpu": [ "x64" ], @@ -1330,9 +1173,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", - "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.38.0.tgz", + "integrity": "sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==", "cpu": [ "arm64" ], @@ -1344,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", - "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.38.0.tgz", + "integrity": "sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==", "cpu": [ "x64" ], @@ -1358,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", - "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.38.0.tgz", + "integrity": "sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==", "cpu": [ "arm" ], @@ -1372,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", - "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.38.0.tgz", + "integrity": "sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==", "cpu": [ "arm" ], @@ -1386,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", - "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.38.0.tgz", + "integrity": "sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==", "cpu": [ "arm64" ], @@ -1400,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", - "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.38.0.tgz", + "integrity": "sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==", "cpu": [ "arm64" ], @@ -1414,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", - "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.38.0.tgz", + "integrity": "sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==", "cpu": [ "loong64" ], @@ -1428,9 +1271,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", - "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.38.0.tgz", + "integrity": "sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==", "cpu": [ "ppc64" ], @@ -1442,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", - "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.38.0.tgz", + "integrity": "sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==", "cpu": [ "riscv64" ], @@ -1456,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", - "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.38.0.tgz", + "integrity": "sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==", "cpu": [ "riscv64" ], @@ -1470,9 +1313,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", - "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.38.0.tgz", + "integrity": "sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==", "cpu": [ "s390x" ], @@ -1484,9 +1327,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", - "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.38.0.tgz", + "integrity": "sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==", "cpu": [ "x64" ], @@ -1498,9 +1341,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", - "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.38.0.tgz", + "integrity": "sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==", "cpu": [ "x64" ], @@ -1512,9 +1355,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", - "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.38.0.tgz", + "integrity": "sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==", "cpu": [ "arm64" ], @@ -1526,9 +1369,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", - "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.38.0.tgz", + "integrity": "sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==", "cpu": [ "ia32" ], @@ -1540,9 +1383,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", - "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.38.0.tgz", + "integrity": "sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==", "cpu": [ "x64" ], @@ -1636,9 +1479,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -1723,10 +1566,11 @@ } }, "node_modules/@types/luxon": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", - "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", - "dev": true + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz", + "integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/methods": { "version": "1.1.4", @@ -1741,9 +1585,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.13.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz", + "integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==", "dev": true, "license": "MIT", "dependencies": { @@ -1754,7 +1598,8 @@ "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/oidc-provider": { "version": "8.8.1", @@ -1896,27 +1741,28 @@ } }, "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", "dev": true, + "license": "MIT", "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1936,16 +1782,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "engines": { @@ -1961,14 +1807,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1979,14 +1825,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2003,9 +1849,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "license": "MIT", "engines": { @@ -2017,14 +1863,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2070,16 +1916,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2094,13 +1940,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2125,9 +1971,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", - "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", + "integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2140,7 +1986,7 @@ "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, @@ -2148,8 +1994,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.9", - "vitest": "3.0.9" + "@vitest/browser": "3.1.1", + "vitest": "3.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2176,14 +2022,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2192,13 +2038,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2219,9 +2065,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "dev": true, "license": "MIT", "dependencies": { @@ -2232,13 +2078,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.1.1", "pathe": "^2.0.3" }, "funding": { @@ -2246,13 +2092,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2261,9 +2107,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2274,13 +2120,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -2465,9 +2311,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": [ { @@ -2498,12 +2344,13 @@ } }, "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" @@ -2609,9 +2456,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "dev": true, "funding": [ { @@ -2697,9 +2544,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.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", "dev": true, "funding": [ { @@ -2707,6 +2554,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -2843,13 +2691,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", @@ -3030,9 +2878,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.128", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", + "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "dev": true, "license": "ISC" }, @@ -3074,15 +2922,6 @@ "node": ">=10.0.0" } }, - "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-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -3112,9 +2951,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", - "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3125,31 +2964,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.1", - "@esbuild/android-arm": "0.25.1", - "@esbuild/android-arm64": "0.25.1", - "@esbuild/android-x64": "0.25.1", - "@esbuild/darwin-arm64": "0.25.1", - "@esbuild/darwin-x64": "0.25.1", - "@esbuild/freebsd-arm64": "0.25.1", - "@esbuild/freebsd-x64": "0.25.1", - "@esbuild/linux-arm": "0.25.1", - "@esbuild/linux-arm64": "0.25.1", - "@esbuild/linux-ia32": "0.25.1", - "@esbuild/linux-loong64": "0.25.1", - "@esbuild/linux-mips64el": "0.25.1", - "@esbuild/linux-ppc64": "0.25.1", - "@esbuild/linux-riscv64": "0.25.1", - "@esbuild/linux-s390x": "0.25.1", - "@esbuild/linux-x64": "0.25.1", - "@esbuild/netbsd-arm64": "0.25.1", - "@esbuild/netbsd-x64": "0.25.1", - "@esbuild/openbsd-arm64": "0.25.1", - "@esbuild/openbsd-x64": "0.25.1", - "@esbuild/sunos-x64": "0.25.1", - "@esbuild/win32-arm64": "0.25.1", - "@esbuild/win32-ia32": "0.25.1", - "@esbuild/win32-x64": "0.25.1" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/escalade": { @@ -3255,14 +3094,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3273,7 +3112,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3286,28 +3125,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" @@ -3316,7 +3155,7 @@ "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/globals": { @@ -3513,9 +3352,9 @@ ] }, "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3634,6 +3473,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", @@ -4012,10 +3864,17 @@ } }, "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-escaper": { "version": "2.0.2", @@ -4178,12 +4037,29 @@ } }, "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "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": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.0.0.tgz", + "integrity": "sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/inflight": { @@ -4202,39 +4078,22 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "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-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" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4371,6 +4230,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4402,12 +4268,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "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", @@ -4542,12 +4402,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4595,9 +4449,9 @@ "dev": true }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "dev": true, "license": "MIT", "engines": { @@ -4733,6 +4587,7 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4885,24 +4740,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-url": { @@ -5109,15 +4958,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "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, - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -5137,18 +4977,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.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.2.0.tgz", + "integrity": "sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ==", "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.26.2", + "index-to-position": "^1.0.0", + "type-fest": "^4.37.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5190,12 +5030,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -5627,108 +5461,44 @@ "node": ">= 0.8" } }, + "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", @@ -5753,41 +5523,29 @@ } }, "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" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, "node_modules/resolve-alpn": { @@ -5847,13 +5605,13 @@ } }, "node_modules/rollup": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", - "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz", + "integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -5863,26 +5621,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.37.0", - "@rollup/rollup-android-arm64": "4.37.0", - "@rollup/rollup-darwin-arm64": "4.37.0", - "@rollup/rollup-darwin-x64": "4.37.0", - "@rollup/rollup-freebsd-arm64": "4.37.0", - "@rollup/rollup-freebsd-x64": "4.37.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", - "@rollup/rollup-linux-arm-musleabihf": "4.37.0", - "@rollup/rollup-linux-arm64-gnu": "4.37.0", - "@rollup/rollup-linux-arm64-musl": "4.37.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", - "@rollup/rollup-linux-riscv64-gnu": "4.37.0", - "@rollup/rollup-linux-riscv64-musl": "4.37.0", - "@rollup/rollup-linux-s390x-gnu": "4.37.0", - "@rollup/rollup-linux-x64-gnu": "4.37.0", - "@rollup/rollup-linux-x64-musl": "4.37.0", - "@rollup/rollup-win32-arm64-msvc": "4.37.0", - "@rollup/rollup-win32-ia32-msvc": "4.37.0", - "@rollup/rollup-win32-x64-msvc": "4.37.0", + "@rollup/rollup-android-arm-eabi": "4.38.0", + "@rollup/rollup-android-arm64": "4.38.0", + "@rollup/rollup-darwin-arm64": "4.38.0", + "@rollup/rollup-darwin-x64": "4.38.0", + "@rollup/rollup-freebsd-arm64": "4.38.0", + "@rollup/rollup-freebsd-x64": "4.38.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.38.0", + "@rollup/rollup-linux-arm-musleabihf": "4.38.0", + "@rollup/rollup-linux-arm64-gnu": "4.38.0", + "@rollup/rollup-linux-arm64-musl": "4.38.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.38.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.38.0", + "@rollup/rollup-linux-riscv64-gnu": "4.38.0", + "@rollup/rollup-linux-riscv64-musl": "4.38.0", + "@rollup/rollup-linux-s390x-gnu": "4.38.0", + "@rollup/rollup-linux-x64-gnu": "4.38.0", + "@rollup/rollup-linux-x64-musl": "4.38.0", + "@rollup/rollup-win32-arm64-msvc": "4.38.0", + "@rollup/rollup-win32-ia32-msvc": "4.38.0", + "@rollup/rollup-win32-x64-msvc": "4.38.0", "fsevents": "~2.3.2" } }, @@ -5938,9 +5696,9 @@ "license": "MIT" }, "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==", "dev": true, "license": "ISC", "bin": { @@ -6080,6 +5838,7 @@ "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" @@ -6089,23 +5848,26 @@ "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 + "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.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "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/split2": { "version": "4.2.0", @@ -6132,9 +5894,9 @@ } }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", "dev": true, "license": "MIT" }, @@ -6202,15 +5964,19 @@ } }, "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "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.0" + "min-indent": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -6272,27 +6038,15 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6471,10 +6225,11 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -6497,6 +6252,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6524,6 +6292,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -6531,6 +6322,19 @@ "dev": true, "license": "MIT" }, + "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/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6541,9 +6345,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -6562,7 +6366,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6605,6 +6409,7 @@ "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" @@ -6620,9 +6425,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "dev": true, "license": "MIT", "dependencies": { @@ -6692,9 +6497,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "dev": true, "license": "MIT", "dependencies": { @@ -6748,31 +6553,31 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "debug": "^4.4.0", - "expect-type": "^1.1.0", + "expect-type": "^1.2.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "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.9", + "vite-node": "3.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6788,8 +6593,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/e2e/package.json b/e2e/package.json index 25db3f2902..18af2be951 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.130.3", + "version": "1.131.3", "description": "", "main": "index.js", "type": "module", @@ -25,18 +25,16 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^57.0.0", "exiftool-vendored": "^28.3.1", "globals": "^16.0.0", "jose": "^5.6.3", @@ -49,6 +47,7 @@ "socket.io-client": "^4.7.4", "supertest": "^7.0.0", "typescript": "^5.3.3", + "typescript-eslint": "^8.28.0", "utimes": "^5.2.1", "vitest": "^3.0.0" }, diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 8700356256..100c2e8cdb 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -1257,6 +1257,7 @@ describe('/asset', () => { for (const { id, status } of assets) { expect(status).toBe(AssetMediaStatus.Created); + // longer timeout as the thumbnail generation from full-size raw files can take a while await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); } diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index e08079ebf3..59ff74cc43 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -329,7 +329,7 @@ describe('/libraries', () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], - exclusionPatterns: ['**/directoryA'], + exclusionPatterns: ['**/directoryA/**'], }); await utils.scan(admin.accessToken, library.id); @@ -337,7 +337,82 @@ describe('/libraries', () => { const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); expect(assets.count).toBe(1); - expect(assets.items[0].originalPath.includes('directoryB')); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining('directoryB/assetB.png') }), + ]), + ); + }); + + it('should scan external library with multiple exclusion patterns', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + exclusionPatterns: ['**/directoryA/**', '**/directoryB/**'], + }); + + await utils.scan(admin.accessToken, library.id); + + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(0); + + expect(assets.items).toEqual([]); + }); + + it('should remove assets covered by a new exclusion pattern', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining('directoryA/assetA.png') }), + expect.objectContaining({ originalPath: expect.stringContaining('directoryB/assetB.png') }), + ]), + ); + } + + await utils.updateLibrary(admin.accessToken, library.id, { + exclusionPatterns: ['**/directoryA/**'], + }); + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(1); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining('directoryB/assetB.png') }), + ]), + ); + } + + await utils.updateLibrary(admin.accessToken, library.id, { + exclusionPatterns: ['**/directoryA/**', '**/directoryB/**'], + }); + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(0); + + expect(assets.items).toEqual([]); + } }); it('should scan multiple import paths', async () => { diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index 154f190f53..afad771bfc 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -117,7 +117,7 @@ describe('/shared-links', () => { const resp = await request(shareUrl).get(`/${linkWithAssets.key}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); - expect(resp.text).toContain(` { + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000); diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index 09340e98cb..37e691625a 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts @@ -44,7 +44,7 @@ test.describe('Photo Viewer', () => { const { x, y, width, height } = box!; await page.mouse.move(x + width / 2, y + height / 2); await page.mouse.wheel(0, -1); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('original'); + await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('fullsize'); }); test('reloads photo when checksum changes', async ({ page }) => { diff --git a/i18n/ar.json b/i18n/ar.json index 66e4045606..0dffc38e68 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -987,6 +987,7 @@ "permanently_deleted_asset": "تم حذف الأصل بشكل نهائي", "permanently_deleted_assets_count": "تم حذف {count, plural, one {# محتوى} other {# المحتويات}} نهائيًا", "person": "شخص", + "person_birthdate": "تاريخ الميلاد {التاريخ}", "person_hidden": "{name}{hidden, select, true { (مخفي)} other {}}", "photo_shared_all_users": "يبدو أنك شاركت صورك مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.", "photos": "الصور", @@ -1078,6 +1079,8 @@ "remove_from_album": "إزالة من الألبوم", "remove_from_favorites": "إزالة من المفضلة", "remove_from_shared_link": "إزالة من الرابط المشترك", + "remove_memory": "إزالة الذاكرة", + "remove_photo_from_memory": "إزالة الصورة من هذه الذكرى", "remove_url": "إزالة عنوان URL", "remove_user": "إزالة المستخدم", "removed_api_key": "تم إزالة مفتاح API: {name}", @@ -1148,6 +1151,7 @@ "searching_locales": "جارٍ البحث في اللغات...", "second": "ثانية", "see_all_people": "عرض جميع الأشخاص", + "select": "إختر", "select_album_cover": "تحديد غلاف الألبوم", "select_all": "تحديد الكل", "select_all_duplicates": "تحديد جميع النسخ المكررة", diff --git a/i18n/en.json b/i18n/en.json index e08c8ec545..de17cccebd 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -65,8 +65,13 @@ "forcing_refresh_library_files": "Forcing refresh of all library files", "image_format": "Format", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", + "image_fullsize_enabled": "Enable full-size image generation", + "image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.", + "image_fullsize_quality_description": "Full-size image quality from 1-100. Higher is better, but produces larger files.", + "image_fullsize_title": "Full-size Image Settings", + "image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in", "image_prefer_embedded_preview": "Prefer embedded preview", - "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.", + "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing and when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.", "image_prefer_wide_gamut": "Prefer wide gamut", "image_prefer_wide_gamut_setting_description": "Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.", "image_preview_description": "Medium-size image with stripped metadata, used when viewing a single asset and for machine learning", @@ -859,6 +864,7 @@ "loop_videos": "Loop videos", "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", "main_branch_warning": "You’re using a development version; we strongly recommend using a release version!", + "main_menu": "Main menu", "make": "Make", "manage_shared_links": "Manage shared links", "manage_sharing_with_partners": "Manage sharing with partners", diff --git a/i18n/eu.json b/i18n/eu.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/eu.json @@ -0,0 +1 @@ +{} diff --git a/i18n/gl.json b/i18n/gl.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/gl.json @@ -0,0 +1 @@ +{} diff --git a/i18n/hi.json b/i18n/hi.json index 9306777c92..c8d9546fee 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -50,6 +50,7 @@ "confirm_user_password_reset": "क्या आप वाकई {user} का पासवर्ड रीसेट करना चाहते हैं?", "create_job": "जॉब बनाएँ", "cron_expression": "क्रॉन अभिव्यक्ति", + "cron_expression_description": "क्रॉन प्रारूप का उपयोग करके स्कैनिंग अंतराल सेट करें। अधिक जानकारी के लिए कृपया क्रोनटैब गुरु देखें", "disable_login": "लॉगिन अक्षम करें", "duplicate_detection_job_description": "समान छवियों का पता लगाने के लिए संपत्तियों पर मशीन लर्निंग चलाएं। यह कार्यक्षमता स्मार्ट खोज पर निर्भर करती है", "exclusion_pattern_description": "Exclusion पैटर्न आपको अपनी लाइब्रेरी को स्कैन करते समय फ़ाइलों और फ़ोल्डरों को अनदेखा करने देता है। यह उपयोगी है यदि आपके पास ऐसे फ़ोल्डर हैं जिनमें ऐसी फ़ाइलें हैं जिन्हें आप आयात नहीं करना चाहते हैं, जैसे RAW फ़ाइलें।", @@ -61,11 +62,14 @@ "failed_job_command": "कार्य {job} के लिए आदेश {command} विफल", "force_delete_user_warning": "चेतावनी: इससे उपयोगकर्ता और सारा डेटा तुरंत हट जाएगा। इसे पूर्ववत नहीं किया जा सकता और फ़ाइलें पुनर्प्राप्त नहीं की जा सकतीं।", "forcing_refresh_library_files": "सभी लाइब्रेरी फ़ाइलों को जबरन सामयिक करें", + "image_format": "प्रारूप", "image_format_description": "वेबपी, जेपीईजी की तुलना में छोटी फ़ाइलें बनाता है, लेकिन एनकोड करने में धीमा है।", "image_prefer_embedded_preview": "एम्बेडेड पूर्वावलोकन को प्राथमिकता दें", "image_prefer_embedded_preview_setting_description": "जब उपलब्ध हो तो RAW फ़ोटो में एम्बेडेड पूर्वावलोकन का उपयोग इमेज प्रोसेसिंग के इनपुट के रूप में करें। यह कुछ छवियों के लिए अधिक सटीक रंग उत्पन्न कर सकता है, लेकिन पूर्वावलोकन की गुणवत्ता कैमरे पर निर्भर करती है और छवि में अधिक संपीड़न कलाकृतियाँ हो सकती हैं।", "image_prefer_wide_gamut": "विस्तृत सरगम को प्राथमिकता दें", "image_prefer_wide_gamut_setting_description": "थंबनेल के लिए डिस्प्ले P3 का उपयोग करें। यह विस्तृत कलरस्पेस वाली छवियों की जीवंतता को बेहतर ढंग से संरक्षित करता है, लेकिन पुराने ब्राउज़र संस्करण वाले पुराने डिवाइस पर छवियां अलग-अलग दिखाई दे सकती हैं। रंग परिवर्तन से बचने के लिए sRGB छवियों को sRGB के रूप में रखा जाता है।", + "image_preview_description": "मेटाडेटा रहित मध्यम आकार की छवि, जिसका उपयोग एकल संपत्ति देखने और मशीन लर्निंग के लिए होता है", + "image_preview_title": "पूर्वदर्शन सेटिंग्स", "image_quality": "गुणवत्ता", "image_settings": "छवि सेटिंग्स", "image_settings_description": "उत्पन्न छवियों की गुणवत्ता और रिज़ॉल्यूशन प्रबंधित करें", diff --git a/i18n/hu.json b/i18n/hu.json index 91d602dd22..4d59a6d17f 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -35,7 +35,7 @@ "authentication_settings_disable_all": "Biztosan letiltod az összes bejelentkezési módot? A bejelentkezés teljesen le lesz tiltva.", "authentication_settings_reenable": "Az újbóli engedélyezéshez használj egySzerver Parancsot.", "background_task_job": "Háttérfeladatok", - "backup_database": "Tartalék Adatbázis", + "backup_database": "Adatbázis Biztonsági Mentése", "backup_database_enable_description": "Adatbázis biztonsági mentések engedélyezése", "backup_keep_last_amount": "Megőrizendő korábbi biztonsági mentések száma", "backup_settings": "Biztonsági mentés beállításai", diff --git a/i18n/ja.json b/i18n/ja.json index 154e6be42e..b0d3a49126 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -7,7 +7,7 @@ "actions": "アクション", "active": "アクティブ", "activity": "アクティビティ", - "activity_changed": "アクティビティは{enabled, select, true {有効化} other {無効化}}されました", + "activity_changed": "アクティビティは{enabled, select, true {有効} other {無効}}になりました", "add": "追加", "add_a_description": "説明を追加", "add_a_location": "場所を追加", @@ -20,20 +20,28 @@ "add_partner": "パートナーを追加", "add_path": "パスを追加", "add_photos": "写真を追加", - "add_to": "追加先...", + "add_to": "追加先…", "add_to_album": "アルバムに追加", "add_to_shared_album": "共有アルバムに追加", + "add_url": "URLを追加", "added_to_archive": "アーカイブに追加済", "added_to_favorites": "お気に入りに追加済", "added_to_favorites_count": "{count, number} 枚の画像をお気に入りに追加済", "admin": { "add_exclusion_pattern_description": "除外パターンを追加します。ワイルドカード「*」「**」「?」を使用できます。すべてのディレクトリで「Raw」と名前が付いたファイルを無視するには、「**/Raw/**」を使用します。また、「.tif」で終わるファイルをすべて無視するには、「**/*.tif」を使用します。さらに、絶対パスを無視するには「/path/to/ignore/**」を使用します。", + "asset_offline_description": "この外部ライブラリのアセットはディスク上に見つからなくなってゴミ箱に移動されました。ファイルがライブラリの中で移動された場合はタイムラインで新しい対応するアセットを確認してください。このアセットを復元するには以下のファイルパスがImmichからアクセスできるか確認してライブラリをスキャンしてください。", "authentication_settings": "認証設定", "authentication_settings_description": "認証設定の管理(パスワード、OAuth、その他)", "authentication_settings_disable_all": "本当に全てのログイン方法を無効にしますか? ログインは完全に無効になります。", "authentication_settings_reenable": "再び有効にするには、サーバーコマンドを使用してください。", "background_task_job": "バックグラウンドタスク", + "backup_database": "データベースのバックアップ", + "backup_database_enable_description": "データベースのバックアップを有効にする", + "backup_keep_last_amount": "過去のバックアップの保持数", + "backup_settings": "バックアップ設定", + "backup_settings_description": "データベースのバックアップ設定の管理", "check_all": "すべてを選択", + "cleanup": "クリーンアップ", "cleared_jobs": "{job}のジョブをクリアしました", "config_set_by_file": "設定は現在 Config File で設定されている", "confirm_delete_library": "本当に {library} を削除しますか?", @@ -41,6 +49,10 @@ "confirm_email_below": "確認のため、以下に \"{email}\" と入力してください", "confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか? これにより名前が付けられた人物も消去されます。", "confirm_user_password_reset": "本当に {user} のパスワードをリセットしますか?", + "create_job": "ジョブの作成", + "cron_expression": "Cron式", + "cron_expression_description": "cronのフォーマットを使ってスキャン間隔を設定します。詳しくはCrontab Guruなどを参照してください", + "cron_expression_presets": "Cron式のプリセット", "disable_login": "ログインを無効にする", "duplicate_detection_job_description": "機械学習を用いて類似画像の検出を行います。(スマートサーチに依存)", "exclusion_pattern_description": "除外パターンを使用すると、ライブラリをスキャンする際にファイルやフォルダを無視することができます。RAWファイルなど、インポートしたくないファイルを含むフォルダがある場合に便利です。", @@ -52,15 +64,25 @@ "failed_job_command": "ジョブ {job}のコマンド {command}が失敗しました", "force_delete_user_warning": "警告:この操作を行うと、ユーザーとすべてのアセットが直ちに削除されます。これは元に戻せず、ファイルも復元できません。", "forcing_refresh_library_files": "すべてのライブラリファイルを強制更新", + "image_format": "フォーマット", "image_format_description": "WebPはJPEGよりもファイルサイズが小さいですが、エンコードに時間がかかります。", "image_prefer_embedded_preview": "埋め込みプレビューを優先", "image_prefer_embedded_preview_setting_description": "RAW写真の埋め込みプレビューが利用可能な場合に画像処理の入力として使用します。これにより、いくつかの画像でより正確な色を得ることができますが、プレビューの品質はカメラによって異なり、画像により多くの圧縮アーティファクトが含まれる場合があります。", "image_prefer_wide_gamut": "広色域に対応させる", "image_prefer_wide_gamut_setting_description": "サムネイルにはDisplay P3を使用します。これにより、広色域の画像の鮮やかさをよりよく保つことができますが、古いデバイスや古いブラウザバージョンでは画像が異なって見える場合があります。sRGBの画像は、色の変化を避けるためにsRGBのままにします。", + "image_preview_description": "単一のアセットを表示する時や機械学習に使われるメタデータを取り除いた中サイズの画像", + "image_preview_quality_description": "プレビューの画質は1〜100で設定できます。値が高いほど品質は良くなりますがファイルサイズが大きくなってアプリの応答性が低下するおそれがあります。低い値を設定すると機械学習の品質に影響を与えるおそれがあります。", + "image_preview_title": "プレビュー設定", "image_quality": "品質", + "image_resolution": "解像度", + "image_resolution_description": "解像度を上げるとより精細に保存できますが、エンコードに時間がかかりファイルサイズが大きくなってアプリの応答性が低下するおそれがあります。", "image_settings": "画像設定", "image_settings_description": "生成される画像の品質と解像度の設定", + "image_thumbnail_description": "メインのタイムラインのような写真グループで表示する際に使われるメタデータを取り除いた小さなサムネイル", + "image_thumbnail_quality_description": "サムネイルの画質を1〜100の間で設定できます。値が大きいほど良い品質ですがファイルサイズが大きくなりアプリの応答性が低下します。", + "image_thumbnail_title": "サムネイル設定", "job_concurrency": "{job} の同時実行数", + "job_created": "ジョブを作成しました", "job_not_concurrency_safe": "このジョブは安全に同時実行できません。", "job_settings": "ジョブ設定", "job_settings_description": "ジョブの同時実行を管理します", @@ -75,7 +97,7 @@ "library_scanning_enable_description": "ライブラリ定期スキャンの有効化", "library_settings": "外部ライブラリ", "library_settings_description": "外部ライブラリ設定を管理します", - "library_tasks_description": "ライブラリのタスクを実行する", + "library_tasks_description": "アセットが追加または変更された外部ライブラリをスキャンする", "library_watching_enable_description": "外部ライブラリのファイル変更を監視", "library_watching_settings": "ライブラリ監視(実験的)", "library_watching_settings_description": "変更されたファイルを自動的に監視", @@ -110,7 +132,7 @@ "machine_learning_smart_search_description": "CLIP埋め込みを使用して画像を意味的に検索します", "machine_learning_smart_search_enabled": "スマートサーチを有効にします", "machine_learning_smart_search_enabled_description": "無効にすると、画像はスマートサーチ用にエンコードされません。", - "machine_learning_url_description": "機械学習サーバーのURL", + "machine_learning_url_description": "機械学習サーバーのURL。複数のURLが設定された場合は1つずつサーバーが正常に応答するまで接続を試みます。応答のないサーバーはオンラインになるまで一時的に無視されます。", "manage_concurrency": "同時実行数の管理", "manage_log_settings": "ログ設定を管理します", "map_dark_style": "ダークモード", @@ -126,8 +148,14 @@ "map_settings": "地図", "map_settings_description": "地図設定", "map_style_description": "マップテーマ(style.json)の参照先URL", + "memory_cleanup_job": "メモリーのクリーンアップ", + "memory_generate_job": "メモリーの生成", "metadata_extraction_job": "メタデータの展開", "metadata_extraction_job_description": "GPSや解像度などのメタデータを各アセットから抽出", + "metadata_faces_import_setting": "顔のインポートを有効にする", + "metadata_faces_import_setting_description": "画像のEXIFデータとサイドカーファイルから顔をインポート", + "metadata_settings": "メタデータ設定", + "metadata_settings_description": "メタデータの設定を管理します", "migration_job": "マイグレーション", "migration_job_description": "アセットおよび顔のサムネイルを最新のフォルダ構造に移行します", "no_paths_added": "パスが追加されていません", @@ -182,6 +210,7 @@ "password_settings": "パスワード ログイン", "password_settings_description": "パスワード ログイン設定を管理します", "paths_validated_successfully": "すべてのパスが正常に検証されました", + "person_cleanup_job": "人物のクリーンアップ", "quota_size_gib": "割り当て容量 (GiB)", "refreshing_all_libraries": "すべてのライブラリを更新", "registration": "管理者登録", @@ -192,9 +221,13 @@ "require_password_change_on_login": "初回ログイン時にパスワード変更を要求する", "reset_settings_to_default": "設定をデフォルトにリセットします", "reset_settings_to_recent_saved": "前回の設定値に戻す", + "scanning_library": "ライブラリのスキャン", + "search_jobs": "ジョブを検索…", "send_welcome_email": "ウェルカム メール を送信します", "server_external_domain_settings": "外部ドメイン", "server_external_domain_settings_description": "公開共有リンク用のドメイン( http(s):// を含める)", + "server_public_users": "公開ユーザー", + "server_public_users_description": "共有アルバムにユーザーを追加するとすべてのユーザー (名前とメールアドレス) がリスト化されます。無効にするとユーザーリストは管理者のみ利用可能になります。", "server_settings": "サーバー設定", "server_settings_description": "サーバー設定を管理します", "server_welcome_message": "ウェルカム メッセージ", @@ -210,7 +243,7 @@ "storage_template_hash_verification_enabled_description": "ハッシュ検証の有効化(よくわからなければ、有効にしてください)", "storage_template_migration": "ストレージ テンプレート の移行", "storage_template_migration_description": "現在の{template}を以前にアップロードされたアセットに適用", - "storage_template_migration_info": "テンプレートの変更は新しいアセットにのみ適用されます。 以前にアップロードしたアセットにテンプレートを遡って適用するには、{job} を実行してください。", + "storage_template_migration_info": "ストレージテンプレートは全ての拡張子を小文字に変換します。テンプレートの変更は新しいアセットにのみ適用されます。 以前にアップロードしたアセットにテンプレートを遡って適用するには、{job} を実行してください。", "storage_template_migration_job": "ストレージテンプレート移行ジョブ", "storage_template_more_details": "この機能の詳細については、ストレージテンプレートとその影響を参照してください", "storage_template_onboarding_description": "この機能を有効にすると、ユーザー定義のテンプレートに基づいてファイルが自動で整理されます。 安定性の問題のため、この機能はデフォルトでオフになっています。 詳細については、ドキュメントを参照してください。", @@ -219,6 +252,17 @@ "storage_template_settings_description": "アップロードしたアセットのフォルダ構造とファイル名を管理します", "storage_template_user_label": "{label}はユーザーのストレージラベルです", "system_settings": "システム設定", + "tag_cleanup_job": "タグのクリーンアップ", + "template_email_available_tags": "テンプレートで次の変数を使えます: {tags}", + "template_email_if_empty": "テンプレートが空の場合はデフォルトのメールが使われます。", + "template_email_invite_album": "アルバム招待のテンプレート", + "template_email_preview": "プレビュー", + "template_email_settings": "メールテンプレート", + "template_email_settings_description": "通知のメールテンプレートのカスタムを管理します", + "template_email_update_album": "アルバム更新のテンプレート", + "template_email_welcome": "ウェルカムメールのテンプレート", + "template_settings": "通知テンプレート", + "template_settings_description": "通知のためのカスタムテンプレートを管理します。", "theme_custom_css_settings": "カスタムCSS", "theme_custom_css_settings_description": "CSS を使って Immich のデザインをカスタマイズできます。", "theme_settings": "テーマ設定", @@ -248,6 +292,8 @@ "transcoding_constant_rate_factor": "CRF値 (-crf)", "transcoding_constant_rate_factor_description": "出力動画の品質レベル。H.264の場合は23、HEVCの場合は28、VP9の場合は31、AV1の場合は35が一般的な値です。値が低いほど品質が良くなりますが、ファイルサイズが大きくなります。", "transcoding_disabled_description": "動画をトランスコードしない設定にしますが、これにより一部のクライアントで再生ができなくなる可能性があります", + "transcoding_encoding_options": "エンコードオプション", + "transcoding_encoding_options_description": "エンコードされた動画のコーデック、解像度、画質、その他オプションの設定します", "transcoding_hardware_acceleration": "ハードウェアアクセラレーション", "transcoding_hardware_acceleration_description": "より高速ですが、同じビットレートではより低品質になります(実験的)", "transcoding_hardware_decoding": "ハードウェアデコード", @@ -260,6 +306,8 @@ "transcoding_max_keyframe_interval": "最大キーフレーム間隔", "transcoding_max_keyframe_interval_description": "キーフレーム間の最大フレーム間隔を設定します。値を低くすると圧縮効率が悪化しますが、シーク時間が改善され、動きの速いシーンの品質が向上する場合があります。\"0\" に設定すると、この値が自動的に設定されます。", "transcoding_optimal_description": "設定解像度を超える動画、または容認されていない形式の動画", + "transcoding_policy": "トランスコードポリシー", + "transcoding_policy_description": "動画がいつトランスコードされるかを設定します", "transcoding_preferred_hardware_device": "推奨ハードウェアデバイス", "transcoding_preferred_hardware_device_description": "VAAPI と QSV のみに適用されます。 ハードウェアトランスコードに使用されるdriノードを設定します。", "transcoding_preset_preset": "プリセット (-preset)", @@ -268,7 +316,7 @@ "transcoding_reference_frames_description": "特定のフレームを圧縮するときに参照するフレームの数。より高い値は圧縮効率を改善しますが、エンコードが遅くなります。\"0\" に設定すると、この値が自動的に設定されます。", "transcoding_required_description": "許容されていない動画形式のみ", "transcoding_settings": "動画トランスコード設定", - "transcoding_settings_description": "動画ファイルの解像度とエンコード情報を管理します", + "transcoding_settings_description": "トランスコードする動画とその処理方法を管理します", "transcoding_target_resolution": "解像度", "transcoding_target_resolution_description": "解像度を高くすると細かなディテールを保持できますが、エンコードに時間がかかり、ファイルサイズが大きくなり、アプリの応答性が低下する可能性があります。", "transcoding_temporal_aq": "適応的量子化(Temporal AQ)", @@ -290,6 +338,7 @@ "trash_settings_description": "ごみ箱の設定を管理します", "untracked_files": "追跡されていないファイル", "untracked_files_description": "これらのファイルはアプリケーションによって追跡されていません。これらは移動の失敗、アップロードの中断、またはバグにより取り残されたものである可能性があります", + "user_cleanup_job": "ユーザーのクリーンアップ", "user_delete_delay": "{user}のアカウントとアセットは{delay, plural, one {#日} other {#日}}後に完全に削除されるように予定されます。", "user_delete_delay_settings": "遅延削除", "user_delete_delay_settings_description": "削除実行後、ユーザーのアカウントとアセットが完全に削除されるまでの日数。 ユーザー削除ジョブは深夜に実行され、削除の準備ができているユーザーを確認します。 この設定への変更は、次回の実行時に反映されます。", @@ -345,6 +394,7 @@ "allow_edits": "編集を許可", "allow_public_user_to_download": "一般ユーザーによるダウンロードを許可", "allow_public_user_to_upload": "一般ユーザーによるアップロードを許可", + "alt_text_qr_code": "QRコード画像", "anti_clockwise": "反時計回り", "api_key": "APIキー", "api_key_description": "この値は一回のみ表示されます。 ウィンドウを閉じる前に必ずコピーしてください。", @@ -368,8 +418,9 @@ "asset_offline": "アセットはオフラインです", "asset_offline_description": "このアセットはオフラインです。 Immichはファイルの場所にアクセスできません。 アセットが利用可能であることを確認しライブラリを再スキャンしてください。", "asset_skipped": "スキップ済", + "asset_skipped_in_trash": "ゴミ箱の中", "asset_uploaded": "アップロード済", - "asset_uploading": "アップロード中...", + "asset_uploading": "アップロード中…", "assets": "アセット", "assets_added_count": "{count, plural, one {#個} other {#個}}のアセットを追加しました", "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}のアセットをアルバムに追加しました", @@ -378,7 +429,7 @@ "assets_moved_to_trash_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_permanently_deleted_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました", "assets_removed_count": "{count, plural, one {#個} other {#個}}のアセットを削除しました", - "assets_restore_confirmation": "ごみ箱のアセットをすべて復元してもよろしいですか? この操作を元に戻すことはできません!", + "assets_restore_confirmation": "ごみ箱のアセットをすべて復元してもよろしいですか? この操作を元に戻すことはできません! オフラインのアセットはこの方法では復元できません。", "assets_restored_count": "{count, plural, one {#個} other {#個}}のアセットを復元しました", "assets_trashed_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_were_part_of_album_count": "{count, plural, one {個} other {個}}のアセットは既にアルバムの一部です", @@ -389,6 +440,7 @@ "birthdate_saved": "生年月日が正常に保存されました", "birthdate_set_description": "生年月日は、写真撮影時のこの人物の年齢を計算するために使用されます。", "blurred_background": "ぼやけた背景", + "bugs_and_feature_requests": "バグと機能のリクエスト", "build": "ビルド", "build_image": "ビルドイメージ", "bulk_delete_duplicates_confirmation": "本当に {count, plural, one {#個} other {#個}}の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!", @@ -433,7 +485,9 @@ "comments_are_disabled": "コメントは無効化されています", "confirm": "確認", "confirm_admin_password": "管理者パスワードを確認", + "confirm_delete_face": "本当に『{name}』の顔をアセットから削除しますか?", "confirm_delete_shared_link": "本当にこの共有リンクを削除しますか?", + "confirm_keep_this_delete_others": "このアセット以外のアセットがスタックから削除されます。本当に削除しますか?", "confirm_password": "確認", "contain": "収める", "context": "状況", @@ -474,25 +528,33 @@ "date_range": "日付", "day": "ライトモード", "deduplicate_all": "全て重複排除", + "deduplication_criteria_1": "バイト単位の画像サイズ", + "deduplication_criteria_2": "EXIFデータ数", + "deduplication_info": "重複排除情報", + "deduplication_info_description": "アセットを自動的に選択して重複を一括で削除するには次のようにします:", "default_locale": "デフォルトのロケール", "default_locale_description": "ブラウザのロケールに基づいて日付と数値をフォーマットします", "delete": "削除", "delete_album": "アルバムを削除", "delete_api_key_prompt": "本当にこのAPI キーを削除しますか?", "delete_duplicates_confirmation": "本当にこれらの重複を完全に削除しますか?", + "delete_face": "顔の削除", "delete_key": "キーを削除", "delete_library": "ライブラリを削除", "delete_link": "リンクを削除", + "delete_others": "ほかを削除", "delete_shared_link": "共有リンクを消す", "delete_tag": "タグを削除する", "delete_tag_confirmation_prompt": "本当に{tagName}タグを削除しますか?", "delete_user": "ユーザーを削除", "deleted_shared_link": "共有リンクを削除", + "deletes_missing_assets": "ディスクからなくなったアセットを削除する", "description": "概要欄", "details": "詳細", "direction": "方向", "disabled": "無効", "disallow_edits": "編集を許可しない", + "discord": "Discord", "discover": "探索", "dismiss_all_errors": "全てのエラーを無視", "dismiss_error": "エラーを無視", @@ -501,6 +563,7 @@ "display_original_photos": "オリジナルの写真を表示", "display_original_photos_setting_description": "オリジナルのアセットが Web 互換である場合は、アセットを表示するときにサムネイルではなく元の写真を優先して表示します。これにより写真の表示速度が遅くなる可能性があります。", "do_not_show_again": "このメッセージを再び表示しない", + "documentation": "ドキュメント", "done": "完了", "download": "ダウンロード", "download_include_embedded_motion_videos": "埋め込まれた動画", @@ -543,6 +606,7 @@ "enabled": "有効", "end_date": "終了日", "error": "エラー", + "error_delete_face": "アセットから顔の削除ができませんでした", "error_loading_image": "画像の読み込みエラー", "error_title": "エラー - 問題が発生しました", "errors": { @@ -570,6 +634,7 @@ "failed_to_create_shared_link": "共有リンクを作成できませんでした", "failed_to_edit_shared_link": "共有リンクを編集できませんでした", "failed_to_get_people": "人物を取得できませんでした", + "failed_to_keep_this_delete_others": "ほかのアセットを削除できませんでした", "failed_to_load_asset": "アセットを読み込めませんでした", "failed_to_load_assets": "アセットを読み込めませんでした", "failed_to_load_people": "人物を読み込めませんでした", @@ -621,6 +686,7 @@ "unable_to_get_comments_number": "コメント数を取得できません", "unable_to_get_shared_link": "共有リンクの取得に失敗しました", "unable_to_hide_person": "人物を非表示にできません", + "unable_to_link_motion_video": "モーションビデオをリンクできません", "unable_to_link_oauth_account": "OAuth アカウントをリンクできません", "unable_to_load_album": "アルバムを読み込めません", "unable_to_load_asset_activity": "アセットのアクティビティを読み込めません", @@ -659,6 +725,7 @@ "unable_to_submit_job": "ジョブを送信できません", "unable_to_trash_asset": "アセットをゴミ箱に移動できません", "unable_to_unlink_account": "アカウントのリンクを解除できません", + "unable_to_unlink_motion_video": "モーションビデオのリンクを解除できません", "unable_to_update_album_cover": "アルバムカバーを更新できません", "unable_to_update_album_info": "アルバム情報を更新できません", "unable_to_update_library": "ライブラリを更新できません", @@ -682,6 +749,7 @@ "external": "外部", "external_libraries": "外部ライブラリ", "face_unassigned": "未割り当て", + "failed_to_load_assets": "アセットのロードに失敗しました", "favorite": "お気に入り", "favorite_or_unfavorite_photo": "写真をお気に入りまたはお気に入り解除", "favorites": "お気に入り", @@ -702,10 +770,13 @@ "get_help": "助けを求める", "getting_started": "はじめる", "go_back": "戻る", + "go_to_folder": "フォルダへ", "go_to_search": "検索へ", "group_albums_by": "これでアルバムをグループ化…", + "group_country": "国でグループ化", "group_no": "グループ化なし", "group_owner": "所有者でグループ化", + "group_places_by": "グループ分け...", "group_year": "年でグループ化", "has_quota": "クォータ有り", "hi_user": "こんにちは、{name}( {email})さん", @@ -738,6 +809,7 @@ "include_shared_albums": "共有アルバムを含める", "include_shared_partner_assets": "パートナーがシェアしたアセットを含める", "individual_share": "1枚の共有", + "individual_shares": "個人の共有", "info": "情報", "interval": { "day_at_onepm": "毎日午後1時", @@ -751,6 +823,8 @@ "jobs": "ジョブ", "keep": "保持", "keep_all": "全て保持", + "keep_this_delete_others": "これを残してほかを削除する", + "kept_this_deleted_others": "このアセットを残して{count, plural, other {#件のアセット}}を削除する", "keyboard_shortcuts": "キーボードショートカット", "language": "言語", "language_setting_description": "優先言語を選択してください", @@ -758,12 +832,14 @@ "latest_version": "最新バージョン", "latitude": "緯度", "leave": "標高", + "lens_model": "レンズモデル", "let_others_respond": "他のユーザーの返信を許可する", "level": "レベル", "library": "ライブラリ", "library_options": "ライブラリ設定", "light": "ライトモード", "like_deleted": "いいねが削除されました", + "link_motion_video": "モーションビデオのリンク", "link_options": "リンクのオプション", "link_to_oauth": "OAuthへリンクする", "linked_oauth_account": "リンクされたOAuthアカウント", @@ -782,6 +858,7 @@ "look": "見た目", "loop_videos": "動画をループ", "loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。", + "main_branch_warning": "開発版を使っているようです。リリース版の使用を強く推奨します!", "make": "メーカー", "manage_shared_links": "共有済みのリンクを管理", "manage_sharing_with_partners": "パートナーとの共有を管理します", @@ -814,6 +891,7 @@ "month": "月", "more": "もっと表示", "moved_to_trash": "ゴミ箱に移動しました", + "mute_memories": "メモリーのミュート", "my_albums": "私のアルバム", "name": "名前", "name_or_nickname": "名前またはニックネーム", @@ -843,7 +921,7 @@ "no_results": "結果がありません", "no_results_description": "同義語やより一般的なキーワードを試してください", "no_shared_albums_message": "アルバムを作成して写真や動画を共有しましょう", - "not_in_any_album": "どのアルバムにも入っていません", + "not_in_any_album": "どのアルバムにも入っていない", "note_apply_storage_label_to_previously_uploaded assets": "注意: 以前にアップロードしたアセットにストレージラベルを適用するには以下を実行してください", "note_unlimited_quota": "注: 容量を無制限にするには0を入力してください", "notes": "注意", @@ -851,6 +929,7 @@ "notifications": "通知", "notifications_setting_description": "通知を管理します", "oauth": "OAuth", + "official_immich_resources": "公式Immichリソース", "offline": "オフライン", "offline_paths": "オフラインのパス", "offline_paths_description": "これらの結果は、外部ライブラリの一部ではないファイルを手動で削除したことが原因である可能性があります。", @@ -908,6 +987,7 @@ "permanently_deleted_asset": "アセットを完全に削除しました", "permanently_deleted_assets_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました", "person": "人物", + "person_birthdate": "{date}生まれ", "person_hidden": "{name}{hidden, select, true { (非表示)} other {}}", "photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。", "photos": "写真", @@ -917,6 +997,7 @@ "pick_a_location": "場所を選択", "place": "場所", "places": "撮影場所", + "places_count": "{count, plural, other {{count, number}箇所}}", "play": "再生", "play_memories": "メモリーを再生", "play_motion_photo": "モーションビデオを再生", @@ -976,14 +1057,17 @@ "reassigned_assets_to_new_person": "{count, plural, one {#個} other {#個}}のアセットを新しい人物に割り当てました", "reassing_hint": "選択されたアセットを既存の人物に割り当て", "recent": "最近", + "recent-albums": "最近のアルバム", "recent_searches": "最近の検索", "refresh": "更新", "refresh_encoded_videos": "エンコードされた動画を更新", + "refresh_faces": "顔認識を更新", "refresh_metadata": "メタデータを更新", "refresh_thumbnails": "サムネイルを更新", "refreshed": "更新済", "refreshes_every_file": "すべてのファイルを更新", "refreshing_encoded_video": "エンコードされた動画を更新中", + "refreshing_faces": "顔認識を更新中", "refreshing_metadata": "メタデータを更新中", "regenerating_thumbnails": "サムネイルを再生成中", "remove": "削除", @@ -995,11 +1079,16 @@ "remove_from_album": "アルバムから削除", "remove_from_favorites": "お気に入りから削除", "remove_from_shared_link": "共有リンクから削除", + "remove_memory": "メモリーの削除", + "remove_photo_from_memory": "メモリーから写真を削除", + "remove_url": "URLの削除", "remove_user": "ユーザーを削除", "removed_api_key": "削除されたAPI キー: {name}", "removed_from_archive": "アーカイブから削除されました", "removed_from_favorites": "お気に入りから削除しました", "removed_from_favorites_count": "{count, plural, other {#項目}}お気に入りから削除しました", + "removed_memory": "削除されたメモリー", + "removed_photo_from_memory": "メモリーから削除された写真", "removed_tagged_assets": "{count, plural, one {#個のアセット} other {#個のアセット}}からタグを削除しました", "rename": "リネーム", "repair": "修復", @@ -1008,6 +1097,7 @@ "repository": "リポジトリ", "require_password": "パスワードを要求", "require_user_to_change_password_on_first_login": "ユーザーに初回ログイン時にパスワードの変更を要求する", + "rescan": "再スキャン", "reset": "リセット", "reset_password": "パスワードをリセット", "reset_people_visibility": "人物の非表示設定をリセット", @@ -1030,22 +1120,29 @@ "saved_settings": "設定を保存しました", "say_something": "何か書き込みましょう", "scan_all_libraries": "全てのライブラリをスキャン", + "scan_library": "スキャン", "scan_settings": "スキャン設定", "scanning_for_album": "アルバムをスキャン中…", "search": "検索", "search_albums": "アルバムを検索", "search_by_context": "状況で検索", + "search_by_description": "概要で検索", + "search_by_description_example": "サパでハイキングした日", "search_by_filename": "ファイル名もしくは拡張子で検索", "search_by_filename_example": "例: IMG_1234.JPG もしくは PNG", "search_camera_make": "カメラメーカーを検索…", "search_camera_model": "カメラのモデルを検索…", "search_city": "市町村を検索…", "search_country": "国を検索…", + "search_for": "検索", "search_for_existing_person": "既存の人物を検索", "search_no_people": "人物がいません", "search_no_people_named": "「{name}」という名前の人物がいません", + "search_options": "検索オプション", "search_people": "人物を検索", "search_places": "場所を検索", + "search_rating": "レートで検索...", + "search_settings": "検索設定", "search_state": "都道府県を検索…", "search_tags": "タグを検索...", "search_timezone": "タイムゾーンを検索…", @@ -1054,6 +1151,7 @@ "searching_locales": "ロケールを検索…", "second": "秒", "see_all_people": "全ての人物を見る", + "select": "選択", "select_album_cover": "アルバムカバーを選択", "select_all": "全て選択", "select_all_duplicates": "全ての重複を選択", @@ -1076,6 +1174,7 @@ "server_version": "サーバーバージョン", "set": "設定", "set_as_album_cover": "アルバムカバーとして設定", + "set_as_featured_photo": "人物写真に設定", "set_as_profile_picture": "プロフィール画像として設定", "set_date_of_birth": "生年月日を設定", "set_profile_picture": "プロフィール画像を設定", @@ -1090,6 +1189,7 @@ "shared_from_partner": "{partner} による写真", "shared_link_options": "共有リンクのオプション", "shared_links": "共有リンク", + "shared_links_description": "写真や動画をリンクで共有", "shared_photos_and_videos_count": "{assetCount, plural, other {#個の共有された写真と動画}}", "shared_with_partner": "{partner} と共有しました", "sharing": "共有", @@ -1112,6 +1212,8 @@ "show_person_options": "人物設定を表示", "show_progress_bar": "プログレスバーを表示", "show_search_options": "検索オプションを表示", + "show_shared_links": "共有リンクを表示", + "show_slideshow_transition": "スライドショーのトランジションを表示", "show_supporter_badge": "サポーターバッジ", "show_supporter_badge_description": "サポーターバッジを表示", "shuffle": "ランダム", @@ -1121,6 +1223,8 @@ "sign_up": "登録", "size": "サイズ", "skip_to_content": "コンテンツへスキップ", + "skip_to_folders": "フォルダへスキップ", + "skip_to_tags": "タグへスキップ", "slideshow": "スライドショー", "slideshow_settings": "スライドショー設定", "sort_albums_by": "この順序でアルバムをソート…", @@ -1128,6 +1232,7 @@ "sort_items": "アイテムの数", "sort_modified": "変更日", "sort_oldest": "古い写真", + "sort_people_by_similarity": "似ている順に人物を並び替える", "sort_recent": "最新の写真", "sort_title": "タイトル", "source": "ソース", @@ -1151,12 +1256,17 @@ "submit": "送信", "suggestions": "ユーザーリスト", "sunrise_on_the_beach": "海岸の日の出", + "support": "サポート", + "support_and_feedback": "サポートとフィードバック", + "support_third_party_description": "Immichのインストールはサードパーティーによってパッケージ化されています。遭遇した問題はそのパッケージに起因している可能性があるので以下のリンクを使って最初にそのパッケージに問題を提起してください。", "swap_merge_direction": "統合する方向を入れ替え", "sync": "同期", "tag": "タグ付けする", "tag_assets": "アセットにタグ付けする", "tag_created": "タグ: {tag} を作成しました", + "tag_feature_description": "意味を持たせたタグトでグループ化して写真と動画を閲覧する", "tag_not_found_question": "タグが見つかりませんか? こちらからタグを作成できます", + "tag_people": "人物タグ", "tag_updated": "タグ: {tag} を更新しました", "tagged_assets": "{count, plural, one {#個のアセット} other {#個のアセット}}をタグ付けしました", "tags": "タグ", @@ -1165,15 +1275,19 @@ "theme_selection": "テーマ選択", "theme_selection_description": "ブラウザのシステム設定に基づいてテーマを明色または暗色に自動的に設定します", "they_will_be_merged_together": "これらは一緒に統合されます", + "third_party_resources": "サードパーティーリソース", "time_based_memories": "時間によるメモリー", + "timeline": "タイムライン", "timezone": "タイムゾーン", "to_archive": "アーカイブ", "to_change_password": "パスワードを変更", "to_favorite": "お気に入り", "to_login": "ログイン", + "to_parent": "上位の階層へ", "to_trash": "ゴミ箱", "toggle_settings": "設定をトグル", "toggle_theme": "ダークテーマを切り替え", + "total": "合計", "total_usage": "総使用量", "trash": "ゴミ箱", "trash_all": "全て削除", @@ -1187,10 +1301,13 @@ "unfavorite": "お気に入りから外す", "unhide_person": "人物の非表示を解除", "unknown": "不明", + "unknown_country": "不明な国", "unknown_year": "不明な年", "unlimited": "無制限", + "unlink_motion_video": "モーションビデオのリンクを解除", "unlink_oauth": "OAuthのリンクを解除", "unlinked_oauth_account": "リンクが解除されたOAuthアカウント", + "unmute_memories": "メモリーのミュートを解除", "unnamed_album": "無名のアルバム", "unnamed_album_delete_confirmation": "本当にこのアルバムを削除しますか?", "unnamed_share": "無名の共有", @@ -1231,7 +1348,9 @@ "variables": "変数", "version": "バージョン", "version_announcement_closing": "あなたの友人、Alex", - "version_announcement_message": "こんにちは、親愛なる皆様へ。アプリの新しいバージョンがありますので、構成の不整合を防ぐためにリリースノートにアクセスし、docker-compose.yml、及び.cnvの設定が最新か確認してください。特に自動的にアプリの更新を制御するWatchTowerやその他システムを利用している場合に当てはまります。", + "version_announcement_message": "こんにちは! 新しいバージョンのImmichがリリースされました。特にWatchTowerやImmichインスタンスを自動的に更新する仕組みを設けている場合はリリースノートをよく読んで設定が最新のものになっているか確認してください。", + "version_history": "バージョン履歴", + "version_history_item": "{date}に{version}をインストール", "video": "動画", "video_hover_setting": "ホバー時にサムネイルで動画を再生", "video_hover_setting_description": "マウスが項目の上にあるときに動画のサムネイルを再生します。無効時でも再生アイコンにカーソルを合わせると再生を開始できます。", @@ -1242,7 +1361,9 @@ "view_all": "すべて見る", "view_all_users": "全てのユーザーを確認する", "view_in_timeline": "タイムラインで見る", + "view_link": "リンクを見る", "view_links": "リンクを確認する", + "view_name": "分類", "view_next_asset": "次のアセットを見る", "view_previous_asset": "前のアセットを見る", "view_stack": "ビュースタック", @@ -1251,10 +1372,10 @@ "warning": "警告", "week": "週", "welcome": "ようこそ", - "welcome_to_immich": "immichにようこそ", + "welcome_to_immich": "Immichにようこそ", "year": "年", "years_ago": "{years, plural, one {#年} other {#年}}前", "yes": "はい", "you_dont_have_any_shared_links": "共有リンクはありません", "zoom_image": "画像を拡大" -} \ No newline at end of file +} diff --git a/i18n/ka.json b/i18n/ka.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/ka.json @@ -0,0 +1 @@ +{} diff --git a/i18n/kk.json b/i18n/kk.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/kk.json @@ -0,0 +1 @@ +{} diff --git a/i18n/kn.json b/i18n/kn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/kn.json @@ -0,0 +1 @@ +{} diff --git a/i18n/ko.json b/i18n/ko.json index 29971e5e30..51ae429438 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -41,6 +41,7 @@ "backup_settings": "백업 설정", "backup_settings_description": "데이터베이스 백업 설정 관리", "check_all": "모두 확인", + "cleanup": "정리", "cleared_jobs": "작업 중단: {job}", "config_set_by_file": "현재 설정은 구성 파일에 의해 관리됩니다.", "confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?", @@ -96,7 +97,7 @@ "library_scanning_enable_description": "주기적인 라이브러리 스캔 활성화", "library_settings": "외부 라이브러리", "library_settings_description": "외부 라이브러리 설정 관리", - "library_tasks_description": "라이브러리 구성 및 확인 작업 수행", + "library_tasks_description": "외부 라이브러리에서 새 자산 및/또는 변경된 자산을 검색합니다", "library_watching_enable_description": "외부 라이브러리의 파일 변경 감시", "library_watching_settings": "라이브러리 감시 (실험 기능)", "library_watching_settings_description": "파일 변겅을 자동으로 감지", @@ -147,6 +148,8 @@ "map_settings": "지도", "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", + "memory_cleanup_job": "메모리 정리", + "memory_generate_job": "메모리 생성", "metadata_extraction_job": "메타데이터 추출", "metadata_extraction_job_description": "각 항목에서 GPS, 인물 및 해상도 등의 메타데이터 정보 추출", "metadata_faces_import_setting": "얼굴 가져오기 활성화", @@ -240,7 +243,7 @@ "storage_template_hash_verification_enabled_description": "해시 검증을 활성화합니다. 이 설정의 결과를 확실히 이해하지 않는 한 비활성화하지 마세요.", "storage_template_migration": "스토리지 템플릿 마이그레이션", "storage_template_migration_description": "이전에 업로드된 항목에 현재 {template} 적용", - "storage_template_migration_info": "템플릿 변경 사항은 새 업로드 항목부터 적용됩니다. 기존 항목에도 적용하려면 {job}을 실행하세요.", + "storage_template_migration_info": "저장소 템플릿은 모든 확장자를 소문자로 변환합니다. 템플릿 변경 사항은 새 자산에만 적용됩니다. 이전에 업로드한 자산에 템플릿을 적용하려면 {job}를 실행하세요.", "storage_template_migration_job": "스토리지 템플릿 마이그레이션 작업", "storage_template_more_details": "이 기능에 대한 자세한 내용은 스토리지 템플릿설명을 참조하세요.", "storage_template_onboarding_description": "이 기능을 활성화하면 사용자 정의 템플릿을 사용하여 파일을 자동으로 정리할 수 있습니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화되어 있습니다. 자세한 내용은 문서를 참조하세요.", @@ -250,10 +253,16 @@ "storage_template_user_label": "사용자의 스토리지 레이블: {label}", "system_settings": "시스템 설정", "tag_cleanup_job": "태그 정리", + "template_email_available_tags": "템플릿에서 다음 변수를 사용할 수 있습니다: {tags}", "template_email_if_empty": "비어 있는 경우 기본 템플릿이 사용됩니다.", + "template_email_invite_album": "앨범 템플릿 초대", "template_email_preview": "미리보기", "template_email_settings": "이메일 템플릿", "template_email_settings_description": "사용자 정의 이메일 템플릿 관리", + "template_email_update_album": "앨범 템플릿 업데이트", + "template_email_welcome": "이메일 템플릿에 오신것을 환영합니다", + "template_settings": "알림 템플릿", + "template_settings_description": "알림을 위한 사용자 지정 템플릿을 관리합니다.", "theme_custom_css_settings": "사용자 정의 CSS", "theme_custom_css_settings_description": "Immich에 적용할 사용자 정의 CSS(Cascading Style Sheets) 설정", "theme_settings": "테마 설정", @@ -278,11 +287,13 @@ "transcoding_audio_codec_description": "Opus는 가장 좋은 품질의 옵션이지만 기기 및 소프트웨어가 오래된 경우 호환되지 않을 수 있습니다.", "transcoding_bitrate_description": "최대 비트레이트를 초과하는 동영상 또는 허용되지 않는 형식의 동영상", "transcoding_codecs_learn_more": "여기에서 사용되는 용어에 대한 자세한 내용은 FFmpeg 문서의 H.264 코덱, HEVC 코덱VP9 코덱 항목을 참조하세요.", - "transcoding_constant_quality_mode": "Constant quality mode", + "transcoding_constant_quality_mode": "고정 품질 모드", "transcoding_constant_quality_mode_description": "ICQ는 CQP보다 나은 성능을 보이나 일부 기기의 하드웨어 가속에서 지원되지 않을 수 있습니다. 이 옵션을 설정하면 품질 기반 인코딩 시 지정된 모드를 우선적으로 사용합니다. NVENC에서는 ICQ를 지원하지 않아 이 설정이 적용되지 않습니다.", - "transcoding_constant_rate_factor": "Constant rate factor (-crf)", + "transcoding_constant_rate_factor": "상수 비율 계수(-CRF)", "transcoding_constant_rate_factor_description": "일반적으로 H.264는 23, HEVC는 28, VP9는 31, AV1는 35를 사용합니다. 값이 낮으면 품질이 향상되지만 파일 크기가 증가합니다.", "transcoding_disabled_description": "동영상을 트랜스코딩하지 않음. 일부 기기에서 재생이 불가능할 수 있습니다.", + "transcoding_encoding_options": "인코딩 옵션", + "transcoding_encoding_options_description": "인코딩된 동영상의 코덱, 해상도, 품질 및 기타 옵션을 설정합니다", "transcoding_hardware_acceleration": "하드웨어 가속", "transcoding_hardware_acceleration_description": "실험적인 기능입니다. 속도가 향상되지만 동일 비트레이트에서 품질이 상대적으로 낮을 수 있습니다.", "transcoding_hardware_decoding": "하드웨어 디코딩", @@ -295,6 +306,8 @@ "transcoding_max_keyframe_interval": "최대 키프레임 간격", "transcoding_max_keyframe_interval_description": "키프레임 사이 최대 프레임 거리를 설정합니다. 값이 낮으면 압축 효율이 저하되지만 검색 시간이 개선되고 빠른 움직임이 있는 장면에서 품질이 향상됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_optimal_description": "목표 해상도보다 높은 동영상 또는 허용되지 않는 형식의 동영상", + "transcoding_policy": "트랜스코드 정책", + "transcoding_policy_description": "동영상 트랜스코딩 시기 설정하기", "transcoding_preferred_hardware_device": "선호하는 하드웨어 기기", "transcoding_preferred_hardware_device_description": "하드웨어 트랜스코딩에 사용할 dri 노드를 설정합니다. (VAAPI와 QSV만 해당)", "transcoding_preset_preset": "프리셋 (-preset)", @@ -303,10 +316,10 @@ "transcoding_reference_frames_description": "특정 프레임을 압축할 때 참조하는 프레임 수를 설정합니다. 값이 높으면 압축 효율이 향상되나 인코딩 속도가 저하됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_required_description": "허용된 형식이 아닌 동영상만", "transcoding_settings": "동영상 트랜스코딩 설정", - "transcoding_settings_description": "동영상 파일의 해상도 및 인코딩 정보 관리", + "transcoding_settings_description": "트랜스코딩할 동영상과 처리 방법 관리하기", "transcoding_target_resolution": "목표 해상도", "transcoding_target_resolution_description": "높은 해상도를 선택한 경우 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "일시적 AQ", "transcoding_temporal_aq_description": "세부 묘사가 많고 움직임이 적은 장면의 품질이 향상됩니다. 오래된 기기와 호환되지 않을 수 있습니다. (NVENC만 해당)", "transcoding_threads": "스레드", "transcoding_threads_description": "값이 높으면 인코딩 속도가 향상되지만 리소스 사용량이 증가합니다. 값은 CPU 코어 수보다 작아야 하며, 설정하지 않으려면 0을 입력합니다.", @@ -381,6 +394,7 @@ "allow_edits": "편집자로 설정", "allow_public_user_to_download": "모든 사용자의 다운로드 허용", "allow_public_user_to_upload": "모든 사용자의 업로드 허용", + "alt_text_qr_code": "QR코드 이미지", "anti_clockwise": "반시계 방향", "api_key": "API 키", "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사해주세요.", @@ -471,7 +485,9 @@ "comments_are_disabled": "댓글이 비활성화되었습니다.", "confirm": "확인", "confirm_admin_password": "관리자 비밀번호 확인", + "confirm_delete_face": "에셋에서 {name} 얼굴을 삭제하시겠습니까?", "confirm_delete_shared_link": "이 공유 링크를 삭제하시겠습니까?", + "confirm_keep_this_delete_others": "이 에셋을 제외한 스택의 다른 모든 에셋이 삭제됩니다. 계속하시겠습니까?", "confirm_password": "비밀번호 확인", "contain": "맞춤", "context": "내용", @@ -512,15 +528,21 @@ "date_range": "날짜 범위", "day": "일", "deduplicate_all": "모두 삭제", + "deduplication_criteria_1": "이미지 크기(바이트)", + "deduplication_criteria_2": "EXIF 데이터 개수", + "deduplication_info": "중복 제거 정보", + "deduplication_info_description": "자산을 자동으로 미리 선택하고 일괄적으로 중복을 제거하려면 다음을 살펴보세요:", "default_locale": "기본 로케일", "default_locale_description": "브라우저 로케일에 따른 날짜 및 숫자 형식 지정", "delete": "삭제", "delete_album": "앨범 삭제", "delete_api_key_prompt": "API 키를 삭제하시겠습니까?", "delete_duplicates_confirmation": "비슷한 항목들을 영구적으로 삭제하시겠습니까?", + "delete_face": "얼굴 삭제", "delete_key": "키 삭제", "delete_library": "라이브러리 삭제", "delete_link": "링크 삭제", + "delete_others": "다른 사람 삭제", "delete_shared_link": "공유 링크 삭제", "delete_tag": "태그 삭제", "delete_tag_confirmation_prompt": "{tagName} 태그를 삭제하시겠습니까?", @@ -532,7 +554,7 @@ "direction": "방향", "disabled": "비활성화됨", "disallow_edits": "뷰어로 설정", - "discord": "Discord", + "discord": "디스코드", "discover": "탐색", "dismiss_all_errors": "모든 오류 무시", "dismiss_error": "오류 무시", @@ -579,11 +601,12 @@ "editor_crop_tool_h2_rotation": "회전", "email": "이메일", "empty_trash": "휴지통 비우기", - "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!", + "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 삭제됩니다.\n이 작업은 되돌릴 수 없습니다!", "enable": "활성화", "enabled": "활성화됨", "end_date": "종료일", "error": "오류", + "error_delete_face": "에셋에서 얼굴 삭제 오류", "error_loading_image": "이미지 로드 오류", "error_title": "오류 - 문제가 발생했습니다", "errors": { @@ -611,6 +634,7 @@ "failed_to_create_shared_link": "공유 링크를 생성하지 못했습니다.", "failed_to_edit_shared_link": "공유 링크를 수정하지 못했습니다.", "failed_to_get_people": "인물 로드 실패", + "failed_to_keep_this_delete_others": "이 자산을 유지하고 다른 자산을 삭제하지 못했습니다", "failed_to_load_asset": "항목 로드 실패", "failed_to_load_assets": "항목 로드 실패", "failed_to_load_people": "인물 로드 실패", @@ -725,6 +749,7 @@ "external": "외부", "external_libraries": "외부 라이브러리", "face_unassigned": "알 수 없음", + "failed_to_load_assets": "에셋 로드에 실패했습니다", "favorite": "즐겨찾기", "favorite_or_unfavorite_photo": "즐겨찾기 추가/제거", "favorites": "즐겨찾기", @@ -745,10 +770,13 @@ "get_help": "도움 요청", "getting_started": "시작하기", "go_back": "뒤로", + "go_to_folder": "폴더로 이동", "go_to_search": "검색으로 이동", "group_albums_by": "다음으로 앨범 그룹화...", + "group_country": "국가별 그룹화", "group_no": "그룹화 없음", "group_owner": "소유자로 그룹화", + "group_places_by": "장소 그룹화 기준...", "group_year": "연도로 그룹화", "has_quota": "할당량", "hi_user": "안녕하세요 {name}님, ({email})", @@ -781,6 +809,7 @@ "include_shared_albums": "공유 앨범 포함", "include_shared_partner_assets": "파트너가 공유한 항목 포함", "individual_share": "개인 공유", + "individual_shares": "개별 공유", "info": "정보", "interval": { "day_at_onepm": "매일 오후 1시", @@ -794,6 +823,8 @@ "jobs": "작업", "keep": "유지", "keep_all": "모두 유지", + "keep_this_delete_others": "이 항목은 보관하고 다른 항목은 삭제", + "kept_this_deleted_others": "이 자산을 유지하고 {count, plural, one {# asset} other {# assets}}을 삭제했습니다", "keyboard_shortcuts": "키보드 단축키", "language": "언어", "language_setting_description": "선호하는 언어 선택", @@ -801,6 +832,7 @@ "latest_version": "최신 버전", "latitude": "위도", "leave": "나가기", + "lens_model": "카메라 렌즈 모델", "let_others_respond": "다른 사용자의 반응 허용", "level": "레벨", "library": "라이브러리", @@ -859,6 +891,7 @@ "month": "월", "more": "더보기", "moved_to_trash": "휴지통으로 이동되었습니다.", + "mute_memories": "추억 음소거", "my_albums": "내 앨범", "name": "이름", "name_or_nickname": "이름 또는 닉네임", @@ -954,6 +987,7 @@ "permanently_deleted_asset": "항목이 영구적으로 삭제되었습니다.", "permanently_deleted_assets_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제되었습니다.", "person": "인물", + "person_birthdate": "{date} 출생", "person_hidden": "{name}{hidden, select, true { (숨김)} other {}}", "photo_shared_all_users": "이미 모든 사용자와 사진을 공유 중이거나 다른 사용자가 없는 것 같습니다.", "photos": "사진", @@ -963,6 +997,7 @@ "pick_a_location": "위치 선택", "place": "장소", "places": "장소", + "places_count": "{count, plural, one {{count, number} 장소} other {{count, number} 장소}}", "play": "재생", "play_memories": "추억 재생", "play_motion_photo": "모션 포토 재생", @@ -1022,6 +1057,7 @@ "reassigned_assets_to_new_person": "항목 {count, plural, one {#개} other {#개}}가 새 인물에 할당되었습니다.", "reassing_hint": "기존 인물에 선택한 항목 할당", "recent": "최근", + "recent-albums": "최근 앨범", "recent_searches": "최근 검색", "refresh": "새로고침", "refresh_encoded_videos": "동영상 재인코딩", @@ -1043,11 +1079,16 @@ "remove_from_album": "앨범에서 제거", "remove_from_favorites": "즐겨찾기에서 제거", "remove_from_shared_link": "공유 링크에서 제거", + "remove_memory": "추억 제거", + "remove_photo_from_memory": "이 추억에서 사진 제거", + "remove_url": "URL 제거", "remove_user": "사용자 삭제", "removed_api_key": "API 키 삭제: {name}", "removed_from_archive": "보관함에서 제거되었습니다.", "removed_from_favorites": "즐겨찾기에서 제거되었습니다.", "removed_from_favorites_count": "즐겨찾기에서 항목 {count, plural, other {#개}} 제거됨", + "removed_memory": "추억 제거", + "removed_photo_from_memory": "이 추억에서 사진 제거", "removed_tagged_assets": "항목 {count, plural, one {#개} other {#개}}에서 태그를 제거함", "rename": "이름 바꾸기", "repair": "수리", @@ -1056,6 +1097,7 @@ "repository": "리포지터리", "require_password": "비밀번호 필요", "require_user_to_change_password_on_first_login": "사용자가 처음 로그인할 때 비밀번호를 변경하도록 요구", + "rescan": "재검색", "reset": "초기화", "reset_password": "비밀번호 재설정", "reset_people_visibility": "인물 표시 여부 초기화", @@ -1092,12 +1134,14 @@ "search_camera_model": "카메라 모델명 검색...", "search_city": "도시 검색...", "search_country": "국가 검색...", + "search_for": "검색", "search_for_existing_person": "존재하는 인물 검색", "search_no_people": "인물이 없습니다.", "search_no_people_named": "\"{name}\" 인물을 찾을 수 없음", "search_options": "검색 옵션", "search_people": "인물 검색", "search_places": "장소 검색", + "search_rating": "등급으로 검색...", "search_settings": "설정 검색", "search_state": "지역 검색...", "search_tags": "태그로 검색...", @@ -1107,6 +1151,7 @@ "searching_locales": "로케일 검색...", "second": "초", "see_all_people": "모든 인물 보기", + "select": "선택", "select_album_cover": "앨범 커버 변경", "select_all": "모두 선택", "select_all_duplicates": "모두 선택", @@ -1129,6 +1174,7 @@ "server_version": "서버 버전", "set": "설정", "set_as_album_cover": "앨범 커버로 설정", + "set_as_featured_photo": "추천 사진으로 설정", "set_as_profile_picture": "프로필 사진으로 설정", "set_date_of_birth": "생년월일 설정", "set_profile_picture": "프로필 사진으로 설정", @@ -1143,6 +1189,7 @@ "shared_from_partner": "{partner}님의 사진", "shared_link_options": "공유 링크 옵션", "shared_links": "공유 링크", + "shared_links_description": "링크를 통해 사진 및 동영상 공유", "shared_photos_and_videos_count": "사진 및 동영상 {assetCount, plural, other {#개를 공유했습니다.}}", "shared_with_partner": "{partner}님과 공유함", "sharing": "공유", @@ -1165,6 +1212,7 @@ "show_person_options": "인물 옵션 표시", "show_progress_bar": "진행 표시줄 표시", "show_search_options": "검색 옵션 표시", + "show_shared_links": "공유 링크 표시", "show_slideshow_transition": "슬라이드 전환 표시", "show_supporter_badge": "서포터 배지", "show_supporter_badge_description": "서포터 배지 표시", @@ -1184,6 +1232,7 @@ "sort_items": "항목 수", "sort_modified": "수정된 날짜", "sort_oldest": "오래된 사진", + "sort_people_by_similarity": "유사성을 기준으로 사람 정렬", "sort_recent": "최근 사진", "sort_title": "제목", "source": "소스", @@ -1217,6 +1266,7 @@ "tag_created": "태그 생성됨: {tag}", "tag_feature_description": "사진 및 동영상을 주제별 그룹화된 태그로 탐색", "tag_not_found_question": "태그를 찾을 수 없나요? 새 태그를 생성하세요.", + "tag_people": "사람 태그", "tag_updated": "태그 업데이트됨: {tag}", "tagged_assets": "항목 {count, plural, one {#개} other {#개}}에 태그를 적용함", "tags": "태그", @@ -1237,6 +1287,7 @@ "to_trash": "삭제", "toggle_settings": "설정 변경", "toggle_theme": "다크 모드 사용", + "total": "합계", "total_usage": "총 사용량", "trash": "휴지통", "trash_all": "모두 삭제", @@ -1256,6 +1307,7 @@ "unlink_motion_video": "모션 비디오 링크 해제", "unlink_oauth": "OAuth 연결 해제", "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.", + "unmute_memories": "추억 음소거 해제", "unnamed_album": "이름 없는 앨범", "unnamed_album_delete_confirmation": "선텍한 앨범을 삭제하시겠습니까?", "unnamed_share": "이름 없는 공유", @@ -1287,6 +1339,7 @@ "user_purchase_settings_description": "구매 및 제품 키 관리", "user_role_set": "{user}님에게 {role} 역할을 설정했습니다.", "user_usage_detail": "사용자 사용량 상세", + "user_usage_stats": "계정 사용량 통계", "user_usage_stats_description": "계정 사용량 통계 보기", "username": "계정명", "users": "사용자", @@ -1308,7 +1361,9 @@ "view_all": "모두 보기", "view_all_users": "모든 사용자 보기", "view_in_timeline": "타임라인에서 보기", + "view_link": "링크 보기", "view_links": "링크 확인", + "view_name": "보기", "view_next_asset": "다음 항목 보기", "view_previous_asset": "이전 항목 보기", "view_stack": "스택 보기", @@ -1323,4 +1378,4 @@ "yes": "네", "you_dont_have_any_shared_links": "생성한 공유 링크가 없습니다.", "zoom_image": "이미지 확대" -} \ No newline at end of file +} diff --git a/i18n/lv.json b/i18n/lv.json index 4d29ce12c2..cd6ae04da9 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -29,6 +29,7 @@ "added_to_favorites_count": "Pievienots {count, number} izlasei", "admin": { "add_exclusion_pattern_description": "Pievienojiet izlaišanas shēmas. Aizstājējzīmju izmantoša *, **, un ? tiek atbalstīta. Lai ignorētu visus failus jebkurā direktorijā ar nosaukumu “RAW”, izmantojiet “**/RAW/**”. Lai ignorētu visus failus, kas beidzas ar “. tif”, izmantojiet “**/*. tif”. Lai ignorētu absolūto ceļu, izmantojiet “/path/to/ignore/**”.", + "asset_offline_description": "Šis ārējās bibliotēkas resurss vairs nav atrodams diskā un ir pārvietots uz atkritumu grozu. Ja fails tika pārvietots bibliotēkas ietvaros, pārbaudiet, vai jūsu hronoloģijā ir jauns atbilstošais resurss. Lai atjaunotu šo resursu, pārliecinieties, vai Immich var piekļūt tālāk norādītajam faila ceļam un skenēt bibliotēku.", "authentication_settings": "Autentifikācijas iestatījumi", "authentication_settings_description": "Paroļu, OAuth un citu autentifikācijas iestatījumu pārvaldība", "authentication_settings_disable_all": "Vai tiešām vēlaties atspējot visas pieteikšanās metodes? Pieteikšanās tiks pilnībā atspējota.", @@ -316,6 +317,8 @@ "birthdate_set_description": "Dzimšanas datums tiek izmantots, lai aprēķinātu šīs personas vecumu fotogrāfijas uzņemšanas brīdī.", "blurred_background": "", "bugs_and_feature_requests": "Kļūdas un funkciju pieprasījumi", + "build": "Būvējums", + "build_image": "Būvējuma attēls", "camera": "", "camera_brand": "", "camera_model": "", @@ -599,7 +602,7 @@ "model": "Modelis", "month": "Mēnesis", "more": "Vairāk", - "moved_to_trash": "", + "moved_to_trash": "Pārvietots uz atkritni", "my_albums": "Mani albumi", "name": "Vārds", "name_or_nickname": "Vārds vai iesauka", @@ -824,7 +827,7 @@ "sort_oldest": "Vecākā fotogrāfija", "sort_recent": "Nesenākā fotogrāfija", "sort_title": "Nosaukums", - "source": "Avots", + "source": "Pirmkods", "stack": "Apvienot kaudzē", "stack_selected_photos": "", "stacktrace": "", @@ -893,6 +896,7 @@ "version": "Versija", "version_announcement_message": "Sveiki! Ir pieejama jauna Immich versija. Lūdzu, veltiet laiku, lai izlasītu laidiena piezīmes un pārliecinātos, ka jūsu iestatījumi ir atjaunināti, lai novērstu jebkādu nepareizu konfigurāciju, jo īpaši, ja izmantojat WatchTower vai citu mehānismu, kas automātiski atjaunina jūsu Immich instanci.", "version_history": "Versiju vēsture", + "version_history_item": "{version} uzstādīta {date}", "video": "Videoklips", "video_hover_setting_description": "", "videos": "Videoklipi", diff --git a/i18n/pa.json b/i18n/pa.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/pa.json @@ -0,0 +1 @@ +{} diff --git a/i18n/sk.json b/i18n/sk.json index d4055f114b..d437787c39 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -1079,6 +1079,8 @@ "remove_from_album": "Odstrániť z albumu", "remove_from_favorites": "Odstrániť z obľúbených", "remove_from_shared_link": "Odstrániť zo zdieľaného odkazu", + "remove_memory": "Odstrániť spomienku", + "remove_photo_from_memory": "Odstrániť fotografiu z tejto spomienky", "remove_url": "Odstrániť URL", "remove_user": "Odstrániť používateľa", "removed_api_key": "Odstrániť API kľúč: {name}", diff --git a/i18n/sq.json b/i18n/sq.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/sq.json @@ -0,0 +1 @@ +{} diff --git a/i18n/sv.json b/i18n/sv.json index 40e9721b69..54eb04e9d2 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -246,7 +246,7 @@ "storage_template_migration_info": "Lagringsmallen kommer konvertera alla filändelser till gemena bokstäver. Ändringar gäller endast för nya resurser, för att retoaktivt tillämpa mallen på befintliga resurser kör {job}.", "storage_template_migration_job": "Lagringsmall migreringsjobb", "storage_template_more_details": "För mer information om den här funktionen se Lagringsmall och dess konsekvenser", - "storage_template_onboarding_description": "Vid aktivering organiserar denna funktion automatiskt filer baserat på en användardefinierad mall. På grunda av stabilitetsproblem är denna funktion avstängd som standard, för mer information se dokumentation.", + "storage_template_onboarding_description": "Vid aktivering organiserar denna funktion automatiskt filer baserat på en användardefinierad mall. På grund av stabilitetsproblem är denna funktion avstängd som standard, för mer information se dokumentation.", "storage_template_path_length": "Uppskattad längdbegränsning på sökväg: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmall", "storage_template_settings_description": "Hantera mappstruktur och filnamn för uppladdade resurser", diff --git a/i18n/te.json b/i18n/te.json index 05f3aaf8bd..0adc561b51 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -20,7 +20,7 @@ "add_partner": "భాగస్వామిని జోడించండి", "add_path": "మార్గాన్ని జోడించండి", "add_photos": "ఫోటోలను జోడించండి", - "add_to": "జోడించండి...", + "add_to": "జోడించండి…", "add_to_album": "ఆల్బమ్‌కు జోడించండి", "add_to_shared_album": "భాగస్వామ్య ఆల్బమ్‌కు జోడించండి", "add_url": "URLని జోడించండి", @@ -41,6 +41,7 @@ "backup_settings": "బ్యాకప్ సెట్టింగ్‌లు", "backup_settings_description": "డేటాబేస్ బ్యాకప్ సెట్టింగ్‌లను నిర్వహించండి", "check_all": "అన్నీ తనిఖీ చేయండి", + "cleanup": "శుభ్రపరచడం", "cleared_jobs": "దీని కోసం ఉద్యోగాలు క్లియర్ చేయబడ్డాయి: {job}", "config_set_by_file": "కాన్ఫిగరేషన్ ప్రస్తుతం కాన్ఫిగరేషన్ ఫైల్ ద్వారా సెట్ చేయబడింది", "confirm_delete_library": "మీరు ఖచ్చితంగా {library} లైబ్రరీని తొలగించాలనుకుంటున్నారా?", @@ -71,10 +72,17 @@ "image_prefer_wide_gamut_setting_description": "థంబ్‌నెయిల్‌ల కోసం డిస్‌ప్లే P3ని ఉపయోగించండి. ఇది విస్తృత రంగుల ఖాళీలతో చిత్రాల వైబ్రెన్స్‌ను మెరుగ్గా భద్రపరుస్తుంది, అయితే పాత బ్రౌజర్ వెర్షన్‌తో పాత పరికరాల్లో చిత్రాలు విభిన్నంగా కనిపించవచ్చు. రంగు మార్పులను నివారించడానికి sRGB చిత్రాలు sRGB వలె ఉంచబడతాయి.", "image_preview_description": "ఒకే ఆస్తిని వీక్షించేటప్పుడు మరియు యంత్ర అభ్యాసం కోసం మెటాడేటా లేని మధ్యస్థ-పరిమాణ చిత్రం ఉపయోగించబడుతుంది", "image_preview_quality_description": "1-100 వరకు ప్రివ్యూ నాణ్యత. ఎక్కువ ఉంటే మంచిది, కానీ పెద్ద ఫైళ్లను ఉత్పత్తి చేస్తుంది మరియు యాప్ ప్రతిస్పందనను తగ్గిస్తుంది. తక్కువ విలువను సెట్ చేయడం వల్ల మెషిన్ లెర్నింగ్ నాణ్యత ప్రభావితం కావచ్చు.", + "image_preview_title": "అమరికల ప్రివ్యూ", "image_quality": "నాణ్యత", + "image_resolution": "రిజల్యూషన్", + "image_resolution_description": "అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరచగలవు కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.", "image_settings": "చిత్రం సెట్టింగ్‌లు", "image_settings_description": "రూపొందించబడిన చిత్రాల నాణ్యత మరియు రిజల్యూషన్‌ను నిర్వహించండి", + "image_thumbnail_description": "తీసివేసిన మెటాడేటాతో కూడిన చిన్న సూక్ష్మచిత్రం, ప్రధాన టైమ్‌లైన్ వంటి ఫోటోల సమూహాలను వీక్షిస్తున్నప్పుడు ఉపయోగించబడుతుంది", + "image_thumbnail_quality_description": "థంబ్‌నెయిల్ నాణ్యత 1-100 నుండి. అధికమైనది ఉత్తమం, కానీ పెద్ద ఫైల్‌లను ఉత్పత్తి చేస్తుంది మరియు యాప్ ప్రతిస్పందనను తగ్గిస్తుంది.", + "image_thumbnail_title": "థంబ్‌నెయిల్ సెట్టింగ్‌లు", "job_concurrency": "{job} సమ్మతి", + "job_created": "పని సృష్టించబడింది", "job_not_concurrency_safe": "ఈ ఉద్యోగం సమ్మతి-సురక్షితమైనది కాదు.", "job_settings": "ఉద్యోగ సెట్టింగ్‌లు", "job_settings_description": "ఉద్యోగ సమ్మతిని నిర్వహించండి", @@ -89,7 +97,7 @@ "library_scanning_enable_description": "ఆవర్తన లైబ్రరీ స్కానింగ్‌ని ప్రారంభించండి", "library_settings": "బాహ్య లైబ్రరీ", "library_settings_description": "బాహ్య లైబ్రరీ సెట్టింగ్‌లను నిర్వహించండి", - "library_tasks_description": "లైబ్రరీ పనులను నిర్వహించండి", + "library_tasks_description": "కొత్త మరియు/లేదా మార్చబడిన ఆస్తుల కోసం బాహ్య లైబ్రరీలను స్కాన్ చేయండి", "library_watching_enable_description": "ఫైల్ మార్పుల కోసం బాహ్య లైబ్రరీలను చూడండి", "library_watching_settings": "లైబ్రరీ చూడటం (ప్రయోగాత్మకం)", "library_watching_settings_description": "మారిన ఫైల్‌ల కోసం ఆటోమేటిక్‌గా చూడండి", @@ -110,7 +118,7 @@ "machine_learning_facial_recognition_model_description": "నమూనాలు పరిమాణం యొక్క అవరోహణ క్రమంలో జాబితా చేయబడ్డాయి. పెద్ద మోడల్‌లు నెమ్మదిగా ఉంటాయి మరియు ఎక్కువ మెమరీని ఉపయోగిస్తాయి, కానీ మంచి ఫలితాలను ఇస్తాయి. మీరు మోడల్‌ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం తప్పనిసరిగా ఫేస్ డిటెక్షన్ జాబ్‌ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.", "machine_learning_facial_recognition_setting": "ముఖ గుర్తింపును ప్రారంభించండి", "machine_learning_facial_recognition_setting_description": "నిలిపివేయబడితే, ముఖ గుర్తింపు కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు మరియు అన్వేషణ పేజీలోని వ్యక్తుల విభాగాన్ని నింపవు.", - "machine_learning_max_detection_distance": "గరిష్ట గుర్తింపు దూరం", + "machine_learning_max_detection_distance": "గరిష్ట కనుగొను దూరం", "machine_learning_max_detection_distance_description": "రెండు చిత్రాల మధ్య గరిష్ట దూరం 0.001-0.1 వరకు నకిలీలుగా పరిగణించబడుతుంది. అధిక విలువలు మరిన్ని నకిలీలను గుర్తిస్తాయి, కానీ తప్పుడు పాజిటివ్‌లకు దారితీయవచ్చు.", "machine_learning_max_recognition_distance": "గరిష్ట గుర్తింపు దూరం", "machine_learning_max_recognition_distance_description": "ఒకే వ్యక్తిగా పరిగణించబడే రెండు ముఖాల మధ్య గరిష్ట దూరం 0-2 వరకు ఉంటుంది. దీన్ని తగ్గించడం ద్వారా ఇద్దరు వ్యక్తులను ఒకే వ్యక్తిగా లేబుల్ చేయడాన్ని నిరోధించవచ్చు, అయితే పెంచడం ద్వారా ఒకే వ్యక్తిని ఇద్దరు వేర్వేరు వ్యక్తులుగా పేర్కొనడాన్ని నిరోధించవచ్చు. ఒక వ్యక్తిని రెండుగా విభజించడం కంటే ఇద్దరు వ్యక్తులను విలీనం చేయడం సులభమని గుర్తుంచుకోండి, కాబట్టి సాధ్యమైనప్పుడు తక్కువ థ్రెషోల్డ్ వైపు తప్పు చేయండి.", @@ -124,36 +132,716 @@ "machine_learning_smart_search_description": "CLIP ఎంబెడ్డింగ్‌లను ఉపయోగించి అర్థపరంగా చిత్రాల కోసం శోధించండి", "machine_learning_smart_search_enabled": "స్మార్ట్ శోధనను ప్రారంభించండి", "machine_learning_smart_search_enabled_description": "నిలిపివేయబడితే, స్మార్ట్ శోధన కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు.", - "machine_learning_url_description": "మెషిన్ లెర్నింగ్ సర్వర్ యొక్క URL", + "machine_learning_url_description": "మెషిన్ లెర్నింగ్ సర్వర్ యొక్క URL. ఒకటి కంటే ఎక్కువ URLలు అందించబడితే, మొదటి నుండి చివరి వరకు క్రమంలో విజయవంతంగా ప్రతిస్పందించే వరకు ప్రతి సర్వర్ ఒక్కోసారి ప్రయత్నించబడుతుంది. ప్రతిస్పందించని సర్వర్‌లు తిరిగి ఆన్‌లైన్‌లోకి వచ్చే వరకు తాత్కాలికంగా విస్మరించబడతాయి.", "manage_concurrency": "కరెన్సీని నిర్వహించండి", "manage_log_settings": "లాగ్ సెట్టింగ్‌లను నిర్వహించండి", "map_dark_style": "చీకటి శైలి", "map_enable_description": "మ్యాప్ లక్షణాలను ప్రారంభించండి", "map_gps_settings": "మ్యాప్ & GPS సెట్టింగ్‌లు", "map_gps_settings_description": "మ్యాప్ & GPS (రివర్స్ జియోకోడింగ్) సెట్టింగ్‌లను నిర్వహించండి", + "map_implications": "మ్యాప్ ఫీచర్ బాహ్య టైల్ సేవపై ఆధారపడి ఉంటుంది (tiles.immich.cloud)", "map_light_style": "పగటి శైలి", "map_manage_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లను నిర్వహించండి", "map_reverse_geocoding": "రివర్స్ జియోకోడింగ్", "map_reverse_geocoding_enable_description": "రివర్స్ జియోకోడింగ్‌ని ప్రారంభించండి", "map_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లు", - "map_settings": "మ్యాప్ సెట్టింగ్‌లు" + "map_settings": "మ్యాప్ సెట్టింగ్‌లు", + "map_settings_description": "మ్యాప్ సెట్టింగ్‌లను నిర్వహించండి", + "map_style_description": "URL నుండి style.json మ్యాప్ థీమ్‌కు", + "memory_cleanup_job": "మెమరీ శుభ్రపరిచడం", + "memory_generate_job": "మెమరీ రూపొందింపు", + "metadata_extraction_job": "మెటాడేటాను సంగ్రహించండి", + "metadata_extraction_job_description": "GPS, ముఖాలు మరియు రిజల్యూషన్ వంటి ప్రతి ఆస్తి నుండి మెటాడేటా సమాచారాన్ని సంగ్రహించండి", + "metadata_faces_import_setting": "ముఖం దిగుమతిని ప్రారంభించండి", + "metadata_faces_import_setting_description": "ఇమేజ్ EXIF డేటా మరియు సైడ్‌కార్ ఫైల్‌ల నుండి ముఖాలను దిగుమతి చేయండి", + "metadata_settings": "మెటాడేటా సెట్టింగ్‌లు", + "metadata_settings_description": "మెటాడేటా సెట్టింగ్‌లను నిర్వహించండి", + "migration_job": "వలస", + "migration_job_description": "ఆస్తులు మరియు ముఖాల కోసం సూక్ష్మచిత్రాలను తాజా ఫోల్డర్ నిర్మాణానికి తరలించండి", + "no_paths_added": "మార్గాలు జోడించబడలేదు", + "no_pattern_added": "నమూనా జోడించబడలేదు", + "note_apply_storage_label_previous_assets": "గమనిక: గతంలో అప్‌లోడ్ చేసిన ఆస్తులకు నిల్వ లేబుల్‌ను వర్తింపజేయడానికి, నడపండి", + "note_cannot_be_changed_later": "గమనిక: దీనిని తర్వాత మార్చలేము!", + "note_unlimited_quota": "గమనిక: అపరిమిత కోటా కోసం 0 నమోదు చేయండి", + "notification_email_from_address": "నుండి", + "notification_email_from_address_description": "పంపినవారి ఇమెయిల్ చిరునామా, ఉదాహరణకు: \"Immich Photo Server \"", + "notification_email_host_description": "ఇమెయిల్ సర్వర్ యొక్క హోస్ట్ (ఉదా. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "సర్టిఫికెట్ లోపాలను విస్మరించండి", + "notification_email_ignore_certificate_errors_description": "TLS సర్టిఫికెట్ ధ్రువీకరణ లోపాలను విస్మరించండి (సిఫార్సు చేయబడలేదు)", + "notification_email_password_description": "ఈమెయిల్ సర్వర్ తో ప్రామాణీకరించేటప్పుడు ఉపయోగించాల్సిన పాస్‌వర్డ్", + "notification_email_port_description": "ఈమెయిల్ సర్వర్ యొక్క పోర్ట్ (ఉదా. 25, 465, లేదా 587)", + "notification_email_sent_test_email_button": "పరీక్ష ఇమెయిల్ పంపి సేవ్ చేయి", + "notification_email_setting_description": "ఇమెయిల్ నోటిఫికేషన్‌లను పంపడానికి సెట్టింగ్‌లు", + "notification_email_test_email": "పరీక్ష ఇమెయిల్ పంపండి", + "notification_email_test_email_failed": "పరీక్ష ఇమెయిల్ పంపడంలో విఫలమైంది, మీ డేటాను తనిఖీ చేయండి", + "notification_email_test_email_sent": "{email} కు ఒక పరీక్ష ఇమెయిల్ పంపబడింది. దయచేసి మీ ఇన్‌బాక్స్‌ని తనిఖీ చేయండి.", + "notification_email_username_description": "ఈమెయిల్ సర్వర్ తో ప్రామాణీకరించేటప్పుడు ఉపయోగించాల్సిన యూజర్ పేరు", + "notification_enable_email_notifications": "ఇమెయిల్ నోటిఫికేషన్‌లను ప్రారంభించండి", + "notification_settings": "నోటిఫికేషన్ సెట్టింగ్‌లు", + "notification_settings_description": "ఇమెయిల్‌తో సహా నోటిఫికేషన్ సెట్టింగ్‌లను నిర్వహించండి", + "oauth_auto_launch": "స్వీయ ప్రారంభం", + "oauth_auto_launch_description": "లాగిన్ పేజీకి నావిగేట్ చేసిన తర్వాత OAuth లాగిన్ ఫ్లోను స్వయంచాలకంగా ప్రారంభించండి", + "oauth_auto_register": "స్వీయ నమోదు", + "oauth_auto_register_description": "OAuth తో సైన్ ఇన్ చేసిన తర్వాత కొత్త వినియోగదారులను స్వయంచాలకంగా నమోదు చేయండి", + "oauth_button_text": "బటన్ వచనం", + "oauth_client_id": "క్లయింట్ ID", + "oauth_client_secret": "క్లయింట్ రహస్యం", + "oauth_enable_description": "OAuth తో లాగిన్ అవ్వండి", + "oauth_issuer_url": "జారీదారు URL", + "oauth_mobile_redirect_uri": "మొబైల్ దారిమార్పు URI", + "oauth_mobile_redirect_uri_override": "మొబైల్ దారిమార్పు URI ఓవర్‌రైడ్", + "oauth_mobile_redirect_uri_override_description": "OAuth ప్రొవైడర్ '{callback}'వంటి మొబైల్ URIని అనుమతించనప్పుడు ప్రారంభించండి", + "oauth_profile_signing_algorithm": "ప్రొఫైల్ సంతకం అల్గోరిథం", + "oauth_profile_signing_algorithm_description": "వినియోగదారు ప్రొఫైల్‌పై సంతకం చేయడానికి ఉపయోగించే అల్గోరిథం.", + "oauth_scope": "పరిధి", + "oauth_settings": "OAuth", + "oauth_settings_description": "OAuth లాగిన్ సెట్టింగ్‌లను నిర్వహించండి", + "oauth_settings_more_details": "ఈ ఫీచర్ గురించి మరిన్ని వివరాల కోసం, డాక్స్ చూడండి.", + "oauth_signing_algorithm": "సంతకం అల్గోరిథం", + "oauth_storage_label_claim": "నిల్వ లేబుల్ క్లెయిమ్", + "oauth_storage_label_claim_description": "యూజర్ యొక్క నిల్వ లేబుల్‌ను ఈ క్లెయిమ్ విలువకు స్వయంచాలకంగా సెట్ చేయండి.", + "oauth_storage_quota_claim": "నిల్వ కోటా క్లెయిమ్", + "oauth_storage_quota_claim_description": "ఈ క్లెయిమ్ విలువకు యూజర్ నిల్వ కోటాను ఆటోమేటిక్‌గా సెట్ చేయండి.", + "oauth_storage_quota_default": "డిఫాల్ట్ నిల్వ కోటా (GiB)", + "oauth_storage_quota_default_description": "క్లెయిమ్ అందించనప్పుడు GiBలోని కోటా ఉపయోగించబడుతుంది (అపరిమిత కోటా కోసం 0 నమోదు చేయండి).", + "offline_paths": "ఆఫ్‌లైన్ మార్గాలు", + "offline_paths_description": "బాహ్య లైబ్రరీలో భాగం కాని ఫైళ్ళను మాన్యువల్‌గా తొలగించడం వల్ల ఈ ఫలితాలు వచ్చి ఉండవచ్చు.", + "password_enable_description": "ఇమెయిల్ మరియు పాస్‌వర్డ్‌తో లాగిన్ అవ్వండి", + "password_settings": "పాస్‌వర్డ్ లాగిన్", + "password_settings_description": "పాస్‌వర్డ్ లాగిన్ సెట్టింగ్‌లను నిర్వహించండి", + "paths_validated_successfully": "అన్ని మార్గాలు విజయవంతంగా ధృవీకరించబడ్డాయి", + "person_cleanup_job": "వ్యక్తి శుభ్రపరచడం", + "quota_size_gib": "కోటా పరిమాణం (GiB)", + "refreshing_all_libraries": "అన్ని లైబ్రరీలను రిఫ్రెష్ చేస్తోంది", + "registration": "నిర్వాహకుల నమోదు", + "registration_description": "మీరు సిస్టమ్‌లో మొదటి వినియోగదారు కాబట్టి, మీరు నిర్వాహకుడిగా నియమించబడతారు మరియు నిర్వాహక పనులకు బాధ్యత వహిస్తారు మరియు అదనపు వినియోగదారులను మీరే సృష్టిస్తారు.", + "repair_all": "అన్నీ బాగు చేయి", + "repair_matched_items": "సరిపోలిన {count, plural, one {# అంశం} other {# అంశాలు}}", + "repaired_items": "మరమ్మతు చేయబడింది {count, plural, one {# అంశం} other {# అంశాలు}}", + "require_password_change_on_login": "మొదటి లాగిన్‌లో యూజర్ పాస్‌వర్డ్ మార్చవలసి ఉంటుంది", + "reset_settings_to_default": "సెట్టింగ్‌లను డిఫాల్ట్‌కు రీసెట్ చేయండి", + "reset_settings_to_recent_saved": "ఇటీవల సేవ్ చేసిన సెట్టింగ్‌లకు సెట్టింగ్‌లను రీసెట్ చేయండి", + "scanning_library": "లైబ్రరీని స్కాన్ చేస్తోంది", + "search_jobs": "ఉద్యోగాల కోసం శోధించండి…", + "send_welcome_email": "స్వాగత ఇమెయిల్ పంపండి", + "server_external_domain_settings": "బాహ్య డొమైన్", + "server_external_domain_settings_description": "http(s):// తో సహా పబ్లిక్ షేర్డ్ లింక్‌ల కోసం డొమైన్", + "server_public_users": "పబ్లిక్ యూజర్లు", + "server_public_users_description": "షేర్డ్ ఆల్బమ్‌లకు యూజర్‌ను జోడించేటప్పుడు అందరు యూజర్లు (పేరు మరియు ఇమెయిల్) జాబితా చేయబడతారు. డిసేబుల్ చేసినప్పుడు, యూజర్ జాబితా అడ్మిన్ యూజర్‌లకు మాత్రమే అందుబాటులో ఉంటుంది.", + "server_settings": "సర్వర్ సెట్టింగ్‌లు", + "server_settings_description": "సర్వర్ సెట్టింగ్‌లను నిర్వహించండి", + "server_welcome_message": "స్వాగత సందేశం", + "server_welcome_message_description": "లాగిన్ పేజీలో ప్రదర్శించబడే సందేశం.", + "sidecar_job": "సైడ్‌కార్ మెటాడేటా", + "sidecar_job_description": "ఫైల్‌సిస్టమ్ నుండి సైడ్‌కార్ మెటాడేటాను కనుగొనండి లేదా సమకాలీకరించండి", + "slideshow_duration_description": "ప్రతి చిత్రాన్ని ఎన్ని సెకన్లు ప్రదర్శించాలి", + "smart_search_job_description": "స్మార్ట్ శోధనకు మద్దతు ఇవ్వడానికి ఆస్తులపై మెషిన్ లెర్నింగ్‌ను అమలు చేయండి", + "storage_template_date_time_description": "తేదీ-సమయ సమాచారం కోసం ఆస్తి సృష్టి సమయ ముద్ర ఉపయోగించబడుతుంది", + "storage_template_date_time_sample": "నమూనా సమయం {date}", + "storage_template_enable_description": "నిల్వ టెంప్లేట్ ఇంజిన్‌ను ప్రారంభించండి", + "storage_template_hash_verification_enabled": "హాష్ ధృవీకరణ ప్రారంభించబడింది", + "storage_template_hash_verification_enabled_description": "హాష్ ధృవీకరణను ప్రారంభిస్తుంది, మీకు దాని చిక్కుల గురించి ఖచ్చితంగా తెలియకపోతే దీన్ని నిలిపివేయవద్దు", + "storage_template_migration": "నిల్వ టెంప్లేట్ మైగ్రేషన్", + "storage_template_migration_description": "గతంలో అప్‌లోడ్ చేసిన ఆస్తులకు ప్రస్తుత {template}ను వర్తింపజేయండి", + "storage_template_migration_info": "నిల్వ టెంప్లేట్ అన్ని పొడిగింపులను చిన్న అక్షరాలకు మారుస్తుంది. టెంప్లేట్ మార్పులు కొత్త ఆస్తులకు మాత్రమే వర్తిస్తాయి. గతంలో అప్‌లోడ్ చేసిన ఆస్తులకు టెంప్లేట్‌ను మునుపు వర్తింపజేయడానికి, {job}ను అమలు చేయండి.", + "storage_template_migration_job": "నిల్వ టెంప్లేట్ మైగ్రేషన్ జాబ్", + "storage_template_more_details": "ఈ ఫీచర్ గురించి మరిన్ని వివరాల కోసం, storage template మరియు దాని implications చూడండి", + "storage_template_onboarding_description": "ప్రారంభించబడినప్పుడు, ఈ లక్షణం వినియోగదారు నిర్వచించిన టెంప్లేట్ ఆధారంగా ఫైళ్ళను స్వయంచాలకంగా నిర్వహిస్తుంది. స్థిరత్వ సమస్యల కారణంగా ఈ లక్షణం డిఫాల్ట్‌గా ఆపివేయబడింది. మరిన్ని వివరాల కోసం, దయచేసి documentation చూడండి.", + "storage_template_path_length": "సుమారు పాత్ పొడవు పరిమితి: {length, number}/{limit, number}", + "storage_template_settings": "నిల్వ టెంప్లేట్", + "storage_template_settings_description": "అప్‌లోడ్ ఆస్తి యొక్క ఫోల్డర్ నిర్మాణం మరియు ఫైల్ పేరును నిర్వహించండి", + "storage_template_user_label": "{label} అనేది వినియోగదారు నిల్వ లేబుల్", + "system_settings": "సిస్టమ్ సెట్టింగ్‌లు", + "tag_cleanup_job": "ట్యాగ్ క్లీనప్", + "template_email_available_tags": "మీరు మీ టెంప్లేట్‌లో ఈ క్రింది వేరియబుల్స్‌ను ఉపయోగించవచ్చు: {tags}", + "template_email_if_empty": "టెంప్లేట్ ఖాళీగా ఉంటే, డిఫాల్ట్ ఇమెయిల్ ఉపయోగించబడుతుంది.", + "template_email_invite_album": "ఆల్బమ్ టెంప్లేట్‌ను ఆహ్వానించండి", + "template_email_preview": "ప్రివ్యూ", + "template_email_settings": "ఇమెయిల్ టెంప్లేట్‌లు", + "template_email_settings_description": "అనుకూల ఇమెయిల్ నోటిఫికేషన్ టెంప్లేట్‌లను నిర్వహించండి", + "template_email_update_album": "ఆల్బమ్ టెంప్లేట్‌ను నవీకరించు", + "template_email_welcome": "స్వాగత ఇమెయిల్ టెంప్లేట్", + "template_settings": "నోటిఫికేషన్ టెంప్లేట్‌లు", + "template_settings_description": "నోటిఫికేషన్‌ల కోసం అనుకూల టెంప్లేట్‌లను నిర్వహించండి.", + "theme_custom_css_settings": "కస్టమ్ CSS", + "theme_custom_css_settings_description": "క్యాస్కేడింగ్ స్టైల్ షీట్‌లు ఇమ్మిచ్ డిజైన్‌ను అనుకూలీకరించడానికి అనుమతిస్తాయి.", + "theme_settings": "థీమ్ సెట్టింగ్‌లు", + "theme_settings_description": "ఇమ్మిచ్ వెబ్ ఇంటర్‌ఫేస్ యొక్క అనుకూలీకరణను నిర్వహించండి", + "these_files_matched_by_checksum": "ఈ ఫైళ్లు వాటి చెక్‌సమ్‌లతో సరిపోల్చబడ్డాయి", + "thumbnail_generation_job": "థంబ్‌నెయిల్‌లను రూపొందించండి", + "thumbnail_generation_job_description": "ప్రతి ఆస్తికి పెద్ద, చిన్న మరియు అస్పష్టమైన థంబ్‌నెయిల్‌లను, అలాగే ప్రతి వ్యక్తికి థంబ్‌నెయిల్‌లను రూపొందించండి", + "transcoding_acceleration_api": "త్వరణం API", + "transcoding_acceleration_api_description": "ట్రాన్స్‌కోడింగ్‌ను వేగవంతం చేయడానికి మీ పరికరంతో సంకర్షణ చెందే API. ఈ సెట్టింగ్ 'ఉత్తమ ప్రయత్నం': విఫలమైనప్పుడు ఇది సాఫ్ట్‌వేర్ ట్రాన్స్‌కోడింగ్‌కు తిరిగి వస్తుంది. మీ హార్డ్‌వేర్‌పై ఆధారపడి VP9 పనిచేయవచ్చు లేదా పనిచేయకపోవచ్చు.", + "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU అవసరం)", + "transcoding_acceleration_qsv": "త్వరిత సమకాలీకరణ (7వ తరం ఇంటెల్ CPU లేదా తదుపరిది అవసరం)", + "transcoding_acceleration_rkmpp": "RKMPP (రాక్‌చిప్ SOCలలో మాత్రమే)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "ఆమోదించబడిన ఆడియో కోడెక్‌లు", + "transcoding_accepted_audio_codecs_description": "ఏ ఆడియో కోడెక్‌లను ట్రాన్స్‌కోడ్ చేయనవసరం లేదో ఎంచుకోండి. కొన్ని ట్రాన్స్‌కోడ్ విధానాలకు మాత్రమే ఉపయోగించబడుతుంది.", + "transcoding_accepted_containers": "ఆమోదించబడిన కంటైనర్లు", + "transcoding_accepted_containers_description": "MP4 కి రీమక్స్ చేయవలసిన అవసరం లేని కంటైనర్ ఫార్మాట్‌లను ఎంచుకోండి. కొన్ని ట్రాన్స్‌కోడ్ విధానాలకు మాత్రమే ఉపయోగించబడుతుంది.", + "transcoding_accepted_video_codecs": "ఆమోదించబడిన వీడియో కోడెక్‌లు", + "transcoding_accepted_video_codecs_description": "ఏ వీడియో కోడెక్‌లను ట్రాన్స్‌కోడ్ చేయనవసరం లేదో ఎంచుకోండి. కొన్ని ట్రాన్స్‌కోడ్ విధానాలకు మాత్రమే ఉపయోగించబడుతుంది.", + "transcoding_advanced_options_description": "చాలా మంది వినియోగదారులు మార్చాల్సిన అవసరం లేని ఎంపికలు", + "transcoding_audio_codec": "ఆడియో కోడెక్", + "transcoding_audio_codec_description": "ఓపస్ అనేది అత్యధిక నాణ్యత గల ఎంపిక, కానీ పాత పరికరాలు లేదా సాఫ్ట్‌వేర్‌లతో తక్కువ అనుకూలతను కలిగి ఉంటుంది.", + "transcoding_bitrate_description": "గరిష్ట బిట్ రేట్ కంటే ఎక్కువ లేదా ఆమోదించబడిన ఫార్మాట్‌లో లేని వీడియోలు", + "transcoding_codecs_learn_more": "ఇక్కడ ఉపయోగించిన పరిభాష గురించి మరింత తెలుసుకోవడానికి, H.264 కోడెక్, HEVC కోడెక్ మరియు VP9 కోడెక్ కోసం FFmpeg డాక్యుమెంటేషన్‌ను చూడండి.", + "transcoding_constant_quality_mode": "స్థిరమైన నాణ్యత మోడ్", + "transcoding_constant_quality_mode_description": "CQP కంటే ICQ మెరుగైనది, కానీ కొన్ని హార్డ్‌వేర్ త్వరణ పరికరాలు ఈ మోడ్‌కు మద్దతు ఇవ్వవు. ఈ ఎంపికను సెట్ చేయడం వలన నాణ్యత-ఆధారిత ఎన్‌కోడింగ్‌ను ఉపయోగిస్తున్నప్పుడు పేర్కొన్న మోడ్‌కు ప్రాధాన్యత ఇవ్వబడుతుంది. ఇది ICQకి మద్దతు ఇవ్వనందున NVENC ద్వారా విస్మరించబడింది.", + "transcoding_constant_rate_factor": "స్థిర రేటు కారకం (-crf)", + "transcoding_constant_rate_factor_description": "వీడియో నాణ్యత స్థాయి. సాధారణ విలువలు H.264 కి 23, HEVC కి 28, VP9 కి 31 మరియు AV1 కి 35. తక్కువ ఉంటే మంచిది, కానీ పెద్ద ఫైళ్లను ఉత్పత్తి చేస్తుంది.", + "transcoding_disabled_description": "ఏ వీడియోలను ట్రాన్స్‌కోడ్ చేయవద్దు, కొన్ని క్లయింట్‌లలో ప్లేబ్యాక్ అంతరాయం కలిగించవచ్చు", + "transcoding_encoding_options": "ఎన్కోడింగ్ ఎంపికలు", + "transcoding_encoding_options_description": "ఎన్కోడ్ చేసిన వీడియోల కోసం కోడెక్‌లు, రిజల్యూషన్, నాణ్యత మరియు ఇతర ఎంపికలను సెట్ చేయండి", + "transcoding_hardware_acceleration": "హార్డ్‌వేర్ త్వరణం", + "transcoding_hardware_acceleration_description": "ప్రయోగాత్మకం; చాలా వేగంగా ఉంటుంది, కానీ అదే బిట్రేట్ వద్ద తక్కువ నాణ్యతను కలిగి ఉంటుంది", + "transcoding_hardware_decoding": "హార్డ్‌వేర్ డీకోడింగ్", + "transcoding_hardware_decoding_setting_description": "కేవలం యాక్సిలరేటింగ్ ఎన్‌కోడింగ్‌కు బదులుగా ఎండ్-టు-ఎండ్ యాక్సిలరేషన్‌ను ప్రారంభిస్తుంది. అన్ని వీడియోలలో పని చేయకపోవచ్చు.", + "transcoding_hevc_codec": "HEVC కోడెక్", + "transcoding_max_b_frames": "గరిష్ట B-ఫ్రేమ్‌లు", + "transcoding_max_b_frames_description": "అధిక విలువలు కుదింపు సామర్థ్యాన్ని మెరుగుపరుస్తాయి, కానీ ఎన్‌కోడింగ్‌ను నెమ్మదిస్తాయి. పాత పరికరాల్లో హార్డ్‌వేర్ త్వరణంతో అనుకూలంగా ఉండకపోవచ్చు. 0 B-ఫ్రేమ్‌లను నిలిపివేస్తుంది, అయితే -1 ఈ విలువను స్వయంచాలకంగా సెట్ చేస్తుంది.", + "transcoding_max_bitrate": "గరిష్ట బిట్రేట్", + "transcoding_max_bitrate_description": "గరిష్ట బిట్రేట్‌ను సెట్ చేయడం వలన ఫైల్ పరిమాణాలను నాణ్యతకు తక్కువ ఖర్చుతో మరింత ఊహించవచ్చు. 720p వద్ద, సాధారణ విలువలు VP9 లేదా HEVCకి 2600 kbit/s, లేదా H.264కి 4500 kbit/s. 0కి సెట్ చేస్తే నిలిపివేయబడుతుంది.", + "transcoding_max_keyframe_interval": "గరిష్ట కీఫ్రేమ్ విరామం", + "transcoding_max_keyframe_interval_description": "కీఫ్రేమ్‌ల మధ్య గరిష్ట ఫ్రేమ్ దూరాన్ని సెట్ చేస్తుంది. తక్కువ విలువలు కుదింపు సామర్థ్యాన్ని మరింత దిగజార్చుతాయి, కానీ సీక్ సమయాలను మెరుగుపరుస్తాయి మరియు వేగవంతమైన కదలికతో దృశ్యాలలో నాణ్యతను మెరుగుపరచవచ్చు. 0 ఈ విలువను స్వయంచాలకంగా సెట్ చేస్తుంది.", + "transcoding_optimal_description": "లక్ష్య రిజల్యూషన్ కంటే ఎక్కువ లేదా ఆమోదించబడిన ఫార్మాట్‌లో లేని వీడియోలు", + "transcoding_policy": "ట్రాన్స్‌కోడ్ విధానం", + "transcoding_policy_description": "వీడియో ఎప్పుడు ట్రాన్స్‌కోడ్ చేయబడుతుందో సెట్ చేయండి", + "transcoding_preferred_hardware_device": "ప్రాధాన్య హార్డ్‌వేర్ పరికరం", + "transcoding_preferred_hardware_device_description": "VAAPI మరియు QSV లకు మాత్రమే వర్తిస్తుంది. హార్డ్‌వేర్ ట్రాన్స్‌కోడింగ్ కోసం ఉపయోగించే dri నోడ్‌ను సెట్ చేస్తుంది.", + "transcoding_preset_preset": "ప్రీసెట్ (-ప్రీసెట్)", + "transcoding_preset_preset_description": "కంప్రెషన్ వేగం. నెమ్మదిగా ఉండే ప్రీసెట్‌లు చిన్న ఫైల్‌లను ఉత్పత్తి చేస్తాయి మరియు నిర్దిష్ట బిట్రేట్‌ను లక్ష్యంగా చేసుకున్నప్పుడు నాణ్యతను పెంచుతాయి. VP9 'వేగవంతమైన' కంటే ఎక్కువ వేగాన్ని విస్మరిస్తుంది.", + "transcoding_reference_frames": "రిఫరెన్స్ ఫ్రేమ్‌లు", + "transcoding_reference_frames_description": "ఇచ్చిన ఫ్రేమ్‌ను కుదించేటప్పుడు సూచించాల్సిన ఫ్రేమ్‌ల సంఖ్య. అధిక విలువలు కుదింపు సామర్థ్యాన్ని మెరుగుపరుస్తాయి, కానీ ఎన్‌కోడింగ్‌ను నెమ్మదిస్తాయి. 0 ఈ విలువను స్వయంచాలకంగా సెట్ చేస్తుంది.", + "transcoding_required_description": "ఆమోదించబడిన ఫార్మాట్‌లో లేని వీడియోలు మాత్రమే", + "transcoding_settings": "వీడియో ట్రాన్స్‌కోడింగ్ సెట్టింగ్‌లు", + "transcoding_settings_description": "ఏ వీడియోలను ట్రాన్స్‌కోడ్ చేయాలో మరియు వాటిని ఎలా ప్రాసెస్ చేయాలో నిర్వహించండి", + "transcoding_target_resolution": "లక్ష్య స్పష్టత", + "transcoding_target_resolution_description": "అధిక రిజల్యూషన్‌లు మరిన్ని వివరాలను భద్రపరచగలవు కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించగలవు.", + "transcoding_temporal_aq": "తాత్కాలిక AQ", + "transcoding_temporal_aq_description": "NVENCకి మాత్రమే వర్తిస్తుంది. అధిక-వివరాలు, తక్కువ-మోషన్ దృశ్యాల నాణ్యతను పెంచుతుంది. పాత పరికరాలతో అనుకూలంగా ఉండకపోవచ్చు.", + "transcoding_threads": "థ్రెడ్‌లు", + "transcoding_threads_description": "అధిక విలువలు వేగవంతమైన ఎన్‌కోడింగ్‌కు దారితీస్తాయి, కానీ సర్వర్ యాక్టివ్‌గా ఉన్నప్పుడు ఇతర పనులను ప్రాసెస్ చేయడానికి తక్కువ స్థలాన్ని వదిలివేస్తుంది. ఈ విలువ CPU కోర్ల సంఖ్య కంటే ఎక్కువగా ఉండకూడదు. 0కి సెట్ చేస్తే వినియోగాన్ని పెంచుతుంది.", + "transcoding_tone_mapping": "టోన్-మ్యాపింగ్", + "transcoding_tone_mapping_description": "SDR కి మార్చినప్పుడు HDR వీడియోల రూపాన్ని సంరక్షించడానికి ప్రయత్నిస్తుంది. ప్రతి అల్గోరిథం రంగు, వివరాలు మరియు ప్రకాశం కోసం వేర్వేరు ట్రేడ్‌ఆఫ్‌లను చేస్తుంది. హేబుల్ వివరాలను సంరక్షిస్తుంది, మోబియస్ రంగును సంరక్షిస్తుంది మరియు రీన్‌హార్డ్ ప్రకాశాన్ని సంరక్షిస్తుంది.", + "transcoding_transcode_policy": "ట్రాన్స్‌కోడ్ విధానం", + "transcoding_transcode_policy_description": "వీడియోను ఎప్పుడు ట్రాన్స్‌కోడ్ చేయాలో పాలసీ. HDR వీడియోలు ఎల్లప్పుడూ ట్రాన్స్‌కోడ్ చేయబడతాయి (ట్రాన్స్‌కోడింగ్ నిలిపివేయబడితే తప్ప).", + "transcoding_two_pass_encoding": "రెండు-పాస్ ఎన్‌కోడింగ్", + "transcoding_two_pass_encoding_setting_description": "మెరుగైన ఎన్‌కోడ్ చేసిన వీడియోలను ఉత్పత్తి చేయడానికి రెండు పాస్‌లలో ట్రాన్స్‌కోడ్ చేయండి. గరిష్ట బిట్‌రేట్ ప్రారంభించబడినప్పుడు (H.264 మరియు HEVCతో పనిచేయడానికి ఇది అవసరం), ఈ మోడ్ గరిష్ట బిట్‌రేట్ ఆధారంగా బిట్‌రేట్ పరిధిని ఉపయోగిస్తుంది మరియు CRFని విస్మరిస్తుంది. VP9 కోసం, గరిష్ట బిట్‌రేట్ నిలిపివేయబడితే CRFని ఉపయోగించవచ్చు.", + "transcoding_video_codec": "వీడియో కోడెక్", + "transcoding_video_codec_description": "VP9 అధిక సామర్థ్యం మరియు వెబ్ అనుకూలతను కలిగి ఉంటుంది, కానీ ట్రాన్స్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది. HEVC కూడా అదేవిధంగా పనిచేస్తుంది, కానీ తక్కువ వెబ్ అనుకూలతను కలిగి ఉంటుంది. H.264 విస్తృతంగా అనుకూలంగా ఉంటుంది మరియు ట్రాన్స్‌కోడ్ చేయడానికి త్వరగా ఉంటుంది, కానీ చాలా పెద్ద ఫైల్‌లను ఉత్పత్తి చేస్తుంది. AV1 అత్యంత సమర్థవంతమైన కోడెక్ కానీ పాత పరికరాల్లో మద్దతు లేదు.", + "trash_enabled_description": "ట్రాష్ ఫీచర్‌లను ప్రారంభించండి", + "trash_number_of_days": "రోజుల సంఖ్య", + "trash_number_of_days_description": "ఆస్తులను శాశ్వతంగా తొలగించే ముందు వాటిని చెత్తలో ఉంచాల్సిన రోజుల సంఖ్య", + "trash_settings": "ట్రాష్ సెట్టింగ్‌లు", + "trash_settings_description": "ట్రాష్ సెట్టింగ్‌లను నిర్వహించండి", + "untracked_files": "ట్రాక్ చేయని ఫైల్‌లు", + "untracked_files_description": "ఈ ఫైళ్లు అప్లికేషన్ ద్వారా ట్రాక్ చేయబడవు. అవి విఫలమైన కదలికలు, అంతరాయం కలిగించిన అప్‌లోడ్‌లు లేదా బగ్ కారణంగా వదిలివేయబడిన ఫలితంగా ఉండవచ్చు", + "user_cleanup_job": "యూజర్ క్లీనప్", + "user_delete_delay": "{user} యొక్క ఖాతా మరియు ఆస్తులు {delay, plural, one {# day} other {# days}} లో శాశ్వత తొలగింపుకు షెడ్యూల్ చేయబడతాయి.", + "user_delete_delay_settings": "ఆలస్యాన్ని తొలగించు", + "user_delete_delay_settings_description": "వినియోగదారు ఖాతా మరియు ఆస్తులను శాశ్వతంగా తొలగించడానికి తీసివేసిన తర్వాత ఎన్ని రోజులు. తొలగింపుకు సిద్ధంగా ఉన్న వినియోగదారులను తనిఖీ చేయడానికి వినియోగదారు తొలగింపు పని అర్ధరాత్రి నడుస్తుంది. ఈ సెట్టింగ్‌కు మార్పులు తదుపరి అమలు సమయంలో మూల్యాంకనం చేయబడతాయి.", + "user_delete_immediately": "{user} ఖాతా మరియు ఆస్తులు శాశ్వత తొలగింపు కోసం వెంటనే వరుసలో ఉంచబడతాయి.", + "user_delete_immediately_checkbox": "తక్షణ తొలగింపు కోసం వినియోగదారు మరియు ఆస్తులను వరుసలో ఉంచండి", + "user_management": "వినియోగదారు నిర్వహణ", + "user_password_has_been_reset": "యూజర్ పాస్‌వర్డ్ రీసెట్ చేయబడింది:", + "user_password_reset_description": "దయచేసి వినియోగదారునికి తాత్కాలిక పాస్‌వర్డ్‌ను అందించండి మరియు వారు తదుపరి లాగిన్ సమయంలో పాస్‌వర్డ్‌ను మార్చవలసి ఉంటుందని వారికి తెలియజేయండి.", + "user_restore_description": "{user} ఖాతా పునరుద్ధరించబడుతుంది.", + "user_restore_scheduled_removal": "వినియోగదారుని పునరుద్ధరించు - {date, date, long}న తొలగింపు షెడ్యూల్ చేయబడింది", + "user_settings": "వాడుకరి సెట్టింగ్‌లు", + "user_settings_description": "వాడుకరి సెట్టింగ్‌లను నిర్వహించండి", + "user_successfully_removed": "వాడుకరి {email} విజయవంతంగా తీసివేయబడింది.", + "version_check_enabled_description": "వర్షన్ తనిఖీని చేయండి", + "version_check_implications": "వర్షన్ తనిఖీ ఫీచర్ github.comతో క్రమానుగత కమ్యూనికేషన్‌పై ఆధారపడుతుంది", + "version_check_settings": "వర్షన్ తనిఖీ", + "version_check_settings_description": "కొత్త వర్షన్ నోటిఫికేషన్‌ను ప్రారంభించండి/ఆపివేయండి", + "video_conversion_job": "వీడియోలను ట్రాన్స్‌కోడ్ చేయండి", + "video_conversion_job_description": "బ్రౌజర్లు మరియు డివైస్‌లతో విస్తృత అనుకూలత కోసం వీడియోలను ట్రాన్స్‌కోడ్ చేయండి" }, + "admin_email": "అడ్మిన్ ఇమెయిల్", + "admin_password": "అడ్మిన్ పాస్‌వర్డ్", + "administration": "పరిపాలన", + "advanced": "ఉన్నతమైన", + "age_months": "వయస్సు {months, plural, one {# నెల} other {# నెలలు}}", + "age_year_months": "వయస్సు 1 సంవత్సరం, {months, plural, one {# నెల} other {# నెలలు}}", + "age_years": "{years, plural, other {వయస్సు #}}", + "album_added": "ఆల్బమ్ జోడించబడింది", + "album_added_notification_setting_description": "మీరు షేర్ చేసిన ఆల్బమ్‌కి జోడించబడినప్పుడు ఇమెయిల్ నోటిఫికేషన్‌ను స్వీకరించండి", + "album_cover_updated": "ఆల్బమ్ కవర్ అప్డేట్ చేయబడింది", + "album_delete_confirmation": "మీరు ఖచ్చితంగా {album} ఆల్బమ్‌ను తొలగించాలనుకుంటున్నారా?", + "album_delete_confirmation_description": "ఈ ఆల్బమ్ షేర్ చేయబడినట్లయితే, ఇతర వినియోగదారులు దీన్ని ఇకపై యాక్సెస్ చేయలేరు.", + "album_info_updated": "ఆల్బమ్ సమాచారం అప్డేట్ చేయబడింది", + "album_leave": "ఆల్బమ్ నుండి బయటకు రావాలా?", + "album_leave_confirmation": "మీరు నిజంగా {album} నుండి బయటకు రావాలనుకుంటున్నారా?", + "album_name": "ఆల్బమ్ పేరు", + "album_options": "ఆల్బమ్ ఎంపికలు", + "album_remove_user": "వాడుకరిని తీసివేయాలా?", + "album_remove_user_confirmation": "మీరు నిజంగా {user} ను తీసివేయాలనుకుంటున్నారా?", + "album_share_no_users": "మీరు ఈ ఆల్బమ్‌ను అన్ని వినియోగదారులతో షేర్ చేసినట్లుగా ఉంది లేదా మీ వద్ద షేర్ చేయడానికి ఎవరూ లేరు.", + "album_updated": "ఆల్బమ్ అప్డేట్ చేయబడింది", + "album_updated_setting_description": "షేర్ చేసిన ఆల్బమ్‌లో కొత్త అంశాలు ఉన్నప్పుడు ఇమెయిల్ నోటిఫికేషన్‌ను స్వీకరించండి", + "album_user_left": "{album} నుండి బయటకు వచ్చారు", + "album_user_removed": "{user} ను తీసివేశారు", + "album_with_link_access": "ఈ లింక్ ఉన్న ఎవరికైనా ఈ ఆల్బమ్‌లోని ఫోటోలు మరియు వ్యక్తులను చూడటానికి అనుమతించండి.", + "albums": "ఆల్బమ్‌లు", + "albums_count": "{count, plural, one {{count, number} ఆల్బమ్} other {{count, number} ఆల్బమ్‌లు}}", + "all": "అన్నీ", + "all_albums": "అన్ని ఆల్బమ్‌లు", + "all_people": "అన్ని వ్యక్తులు", + "all_videos": "అన్ని వీడియోలు", + "allow_dark_mode": "చీకటి మోడ్‌ను అనుమతించండి", + "allow_edits": "మార్పులను అనుమతించండి", + "allow_public_user_to_download": "పబ్లిక్ వినియోగదారుడు డౌన్‌లోడ్ చేసేందుకు అనుమతించండి", + "allow_public_user_to_upload": "పబ్లిక్ వినియోగదారుడు అప్‌లోడ్ చేసేందుకు అనుమతించండి", + "alt_text_qr_code": "క్యూఆర్ కోడ్ చిత్రం", + "anti_clockwise": "ఎడమవైపు తిరిగే దిశ", + "api_key": "API కీ", + "api_key_description": "ఈ విలువ ఒక్కసారి మాత్రమే చూపబడుతుంది. విండోను మూసివేసే ముందు దయచేసి దీనిని ఖచ్చితంగా కాపీ చేసి ఎక్కడైనా భద్రపరచండి.", + "api_key_empty": "మీ API కీ పేరు ఖాళీగా ఉండకూడదు", + "api_keys": "API కీలు", + "app_settings": "యాప్ సెట్టింగ్‌లు", + "appears_in": "లో కనిపిస్తుంది", + "archive": "ఆర్కైవ్", + "archive_or_unarchive_photo": "ఫోటోను ఆర్కైవ్ లేదా అన్‌ఆర్కైవ్ చేయండి", + "archive_size": "ఆర్కైవ్ పరిమాణం", + "archive_size_description": "డౌన్‌లోడ్‌ల కోసం ఆర్కైవ్ పరిమాణాన్ని (GiBలో) కాన్ఫిగర్ చేయండి", + "archived_count": "{count, plural, other {# ఆర్కైవ్ చేయబడింది}}", + "are_these_the_same_person": "వీళ్లందరు ఒకే వ్యక్తినా?", + "are_you_sure_to_do_this": "మీరు నిజంగా ఇది చేయాలనుకుంటున్నారా?", + "asset_added_to_album": "ఆల్బమ్‌కు జోడించబడింది", + "asset_adding_to_album": "ఆల్బమ్‌కు జోడిస్తున్నాము…", + "asset_description_updated": "ఆస్తి వివరణ అప్డేట్ చేయబడింది", + "asset_filename_is_offline": "ఆస్తి {filename} ఆఫ్‌లైన్‌లో ఉంది", + "asset_has_unassigned_faces": "ఆస్తికి కేటాయించని ముఖాలు ఉన్నాయి", + "asset_hashing": "హాషింగ్ చేస్తున్నాము…", + "asset_offline": "ఆస్తి ఆఫ్‌లైన్‌లో ఉంది", + "asset_offline_description": "ఈ బాహ్య ఆస్తి ఇకపై డిస్క్‌లో కనబడటం లేదు. సహాయానికి దయచేసి మీ Immich నిర్వాహకుడిని సంప్రదించండి.", + "asset_skipped": "దాటవేయబడింది", + "asset_skipped_in_trash": "చెత్తబుట్టలో ఉంది", + "asset_uploaded": "అప్‌లోడ్ చేయబడింది", + "asset_uploading": "అప్‌లోడ్ జరుగుతోంది…", + "assets": "ఆస్తులు", + "assets_added_count": "జోడించబడినవి {count, plural, one {# ఆస్తి} other {# ఆస్తులు}}", + "assets_added_to_album_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} ఆల్బమ్‌కి జోడించబడినవి", + "assets_added_to_name_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} {hasName, select, true {{name}} other {కొత్త ఆల్బమ్}}కి జోడించబడినవి", + "assets_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}}", + "assets_moved_to_trash_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} చెత్తబుట్టలోకి తరలించారు", + "assets_permanently_deleted_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} శాశ్వతంగా తొలగించబడినవి", + "assets_removed_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} తీసివేయబడినవి", + "assets_restore_confirmation": "మీరు మీ చెత్తబుట్టలోని అన్ని ఆస్తులను పునరుద్ధరించాలనుకుంటున్నారా? మీరు ఈ చర్యను ఆపలేరు/రద్దు చేయలేరు! దయచేసి గమనించండి, ఆఫ్‌లైన్‌లో ఉన్న ఆస్తులు ఈ విధంగా పునరుద్ధరించబడవు.", + "assets_restored_count": "{count, plural, one {# ఆస్తి} other {# ఆస్తులు}} పునరుద్ధరించబడినవి", + "assets_trashed_count": "తొలగించబడింది {count, plural, one {# అసెట్} other {# అసెట్‌లు}}", + "assets_were_part_of_album_count": "{count, plural, one {అసెట్ ఇప్పటికే} other {అసెట్‌లు ఇప్పటికే}} ఆల్బమ్‌లో భాగంగా ఉన్నాయి", + "authorized_devices": "అనుమతించబడిన పరికరాలు", + "back": "తిరిగి", + "back_close_deselect": "వెనక్కి, మూసివేయి, లేదా ఎంచుకున్నది తీసివేయి", + "backward": "వెనుకకు", + "birthdate_saved": "జన్మతేది విజయవంతంగా సేవ్ చేయబడింది", + "birthdate_set_description": "జన్మతేది ఈ వ్యక్తి ఫోటో తీసిన సమయంలో వయస్సును గణించేందుకు ఉపయోగించబడుతుంది.", + "blurred_background": "ముదురు నేపధ్యం", + "bugs_and_feature_requests": "లోపాలు & ఫీచర్ అభ్యర్థనలు", + "build": "నిర్మాణం", + "build_image": "నిర్మాణ సంఖ్య", + "bulk_delete_duplicates_confirmation": "మీరు నిజంగా {count, plural, one {# నకిలీ అసెట్} other {# నకిలీ అసెట్‌లు}} సమూహంగా తొలగించాలనుకుంటున్నారా? ప్రతి సమూహంలోని అతిపెద్ద అసెట్‌ను ఉంచి, మిగతా అన్ని నకిలీలను శాశ్వతంగా తొలగించబడతాయి. ఈ చర్యను వెనుకకు తీసుకోలేరు!", + "bulk_keep_duplicates_confirmation": "మీరు నిజంగానే {count, plural, one {# నకిలీ అసెట్‌ను} other {# నకిలీ అసెట్‌లను}} ఉంచాలనుకుంటున్నారా? ఇది అన్ని నకిలీ సమూహాలను ఏవీ తొలగించకుండా పరిష్కరిస్తుంది.", + "bulk_trash_duplicates_confirmation": "మీరు నిజంగా {count, plural, one {# నకిలీ అసెట్} other {# నకిలీ అసెట్‌లు}} సమూహంగా చెత్తబుట్టలో వేయాలనుకుంటున్నారా? ప్రతి సమూహంలోని అతిపెద్ద అసెట్‌ను ఉంచి, మిగతా అన్ని నకిలీలను చెత్తబుట్టలో వేయబడతాయి.", + "buy": "Immichను కొనండి", + "camera": "కెమెరా", + "camera_brand": "కెమెరా బ్రాండ్", + "camera_model": "కెమెరా మోడల్", + "cancel": "రద్దు చేయి", + "cancel_search": "వెతకడం రద్దు చేయి", + "cannot_merge_people": "వ్యక్తులను విలీనం చేయలేరు", + "cannot_undo_this_action": "మీరు ఈ చర్యను వెనుకకు తీసుకోలేరు!", + "cannot_update_the_description": "వివరణను నవీకరించలేరు", + "change_date": "తేదీ మార్చు", + "change_expiration_time": "గడువు సమయాన్ని మార్చు", + "change_location": "స్థానాన్ని మార్చు", + "change_name": "పేరు మార్చు", + "change_name_successfully": "పేరు విజయవంతంగా మార్చబడింది", + "change_password": "పాస్వర్డ్ మార్చు", + "change_password_description": "ఇదే మీరు వ్యవస్థలోకి మొట్టమొదటిసారి సైన్ఇన్ చేయడం, లేదా మీ పాస్వర్డ్ మార్చే అభ్యర్ధన చేయబడింది. దయచేసి కొత్త పాస్వర్డ్ కింద ఇవ్వండి.", + "change_your_password": "మీ పాస్వర్డ్‌ను మార్చండి", + "changed_visibility_successfully": "దృశ్యమానత విజయవంతంగా మార్చబడింది", + "check_all": "అన్నింటిని తనిఖీ చేయండి", + "check_logs": "లాగ్‌లను తనిఖీ చేయండి", + "choose_matching_people_to_merge": "విలీనం చేయడానికి సరిపోలిన వ్యక్తులను ఎంచుకోండి", + "city": "నగరం", + "clear": "ఖాళీ చేయి", + "clear_all": "అన్నీ ఖాళీ చేయి", + "clear_all_recent_searches": "ఇటీవల చేసిన అన్ని శోధనలను ఖాళీ చేయి", + "clear_message": "సందేశాన్ని ఖాళీ చేయి", + "clear_value": "విలువను ఖాళీ చేయి", + "clockwise": "సమయదిశగా", + "close": "మూసివేయి", + "collapse": "సంకుచితం చేయి", + "collapse_all": "అన్నీ సంకుచితం చేయి", + "color": "రంగు", + "color_theme": "రంగు థీమ్", + "comment_deleted": "వ్యాఖ్య తొలగించబడింది", + "comment_options": "వ్యాఖ్య ఎంపికలు", + "comments_and_likes": "వ్యాఖ్యలు & లైక్‌లు", + "comments_are_disabled": "వ్యాఖ్యలు అచేతనంగా ఉన్నాయి", + "confirm": "నిర్ధారించండి", + "confirm_admin_password": "అడ్మిన్ పాస్వర్డ్‌ను నిర్ధారించండి", + "confirm_delete_face": "మీరు నిజంగా {name} ముఖాన్ని అసెట్ నుండి తొలగించాలనుకుంటున్నారా?", + "confirm_delete_shared_link": "మీరు నిజంగా ఈ పంచుకున్న లింక్‌ను తొలగించాలనుకుంటున్నారా?", + "confirm_keep_this_delete_others": "స్టాక్‌లోని ఈ అసెట్‌ను మినహాయించి మిగతా అన్ని అసెట్‌లు తొలగించబడతాయి. మీరు నిజంగా కొనసాగించాలనుకుంటున్నారా?", + "confirm_password": "పాస్వర్డ్‌ను నిర్ధారించండి", + "contain": "ఇరికించు", + "context": "సందర్భం", + "continue": "కొనసాగించు", + "copied_image_to_clipboard": "చిత్రాన్ని క్లిప్‌బోర్డ్‌కు కాపీ చేయబడింది.", + "copied_to_clipboard": "క్లిప్‌బోర్డ్‌కు కాపీ చేయబడింది!", + "copy_error": "కాపీ ఎర్రర్", + "copy_file_path": "ఫైల్ పాత్‌ను కాపీ చేయండి", + "copy_image": "చిత్రాన్ని కాపీ చేయి", + "copy_link": "లింక్‌ను కాపీ చేయి", + "copy_link_to_clipboard": "లింక్‌ను క్లిప్‌బోర్డ్‌కు కాపీ చేయండి", + "copy_password": "పాస్‌వర్డ్‌ను కాపీ చేయండి", + "copy_to_clipboard": "క్లిప్‌బోర్డ్‌కు కాపీ చేయి", + "country": "దేశం", + "cover": "నింపు", + "covers": "నింపుతుంది", + "create": "సృష్టించు", + "create_album": "ఆల్బమ్‌ను సృష్టించండి", + "create_library": "లైబ్రరీని సృష్టించండి", + "create_link": "లింక్‌ను సృష్టించండి", + "create_link_to_share": "షేర్ చేయడానికి లింక్‌ను సృష్టించండి", + "create_link_to_share_description": "లింక్ ఉన్న ఎవరైనా ఎంచుకున్న ఫోటో(ల)ను చూడనివ్వండి", + "create_new_person": "కొత్త వ్యక్తిని సృష్టించండి", + "create_new_person_hint": "ఎంచుకున్న ఆస్తులను కొత్త వ్యక్తికి కేటాయించండి", + "create_new_user": "కొత్త వినియోగదారుని సృష్టించండి", + "create_tag": "ట్యాగ్‌ను సృష్టించండి", + "create_tag_description": "కొత్త ట్యాగ్‌ను సృష్టించండి. నెస్టెడ్ ట్యాగ్‌ల కోసం, దయచేసి ఫార్వర్డ్ స్లాష్‌లతో సహా ట్యాగ్ యొక్క పూర్తి మార్గాన్ని నమోదు చేయండి.", + "create_user": "వినియోగదారుని సృష్టించండి", + "created": "సృష్టించబడింది", + "current_device": "ప్రస్తుత పరికరం", + "custom_locale": "అనుకూల భాష", + "custom_locale_description": "భాష మరియు ప్రాంతం ఆధారంగా తేదీలు మరియు సంఖ్యలను ఫార్మాట్ చేయండి", + "dark": "చీకటి", + "date_after": "తేదీ తర్వాత", + "date_and_time": "తేదీ మరియు సమయం", + "date_before": "తేదీ ముందు", + "date_of_birth_saved": "పుట్టిన తేదీ విజయవంతంగా సేవ్ చేయబడింది", + "date_range": "తేదీ పరిధి", + "day": "రోజు", + "deduplicate_all": "అన్నీ నకిలీలు తొలగించు", + "deduplication_criteria_1": "బైట్‌లలో చిత్ర పరిమాణం", + "deduplication_criteria_2": "EXIF డేటా సంఖ్య", + "deduplication_info": "నకిలీల తొలగింపు సమాచారం", + "deduplication_info_description": "ఆస్తులను స్వయంచాలకంగా ముందస్తుగా ఎంచుకోవడానికి మరియు నకిలీలను పెద్దమొత్తంలో తొలగించడానికి, మేము వీటిని పరిశీలిస్తాము:", + "default_locale": "పూర్వనియోజిత భాష", + "default_locale_description": "మీ బ్రౌజర్ లొకేల్ ఆధారంగా తేదీలు మరియు సంఖ్యలను ఫార్మాట్ చేయండి", + "delete": "తొలగించు", + "delete_album": "ఆల్బమ్‌ను తొలగించు", + "delete_api_key_prompt": "మీరు ఈ API కీని ఖచ్చితంగా తొలగించాలనుకుంటున్నారా?", + "delete_duplicates_confirmation": "మీరు ఈ నకిలీలను శాశ్వతంగా తొలగించాలనుకుంటున్నారా?", + "delete_face": "ముఖాన్ని తొలగించు", + "delete_key": "కీని తొలగించు", + "delete_library": "లైబ్రరీని తొలగించు", + "delete_link": "లింక్‌ను తొలగించు", + "delete_others": "ఇతరులను తొలగించండి", + "delete_shared_link": "షేర్ చేసిన లింక్‌ను తొలగించండి", + "delete_tag": "ట్యాగ్‌ను తొలగించు", + "delete_tag_confirmation_prompt": "మీరు ఖచ్చితంగా {tagName} ట్యాగ్‌ను తొలగించాలనుకుంటున్నారా?", + "delete_user": "వినియోగదారుని తొలగించు", + "deleted_shared_link": "షేర్ చేసిన లింక్ తొలగించబడింది", + "deletes_missing_assets": "డిస్క్ నుండి తప్పిపోయిన ఆస్తులను తొలగిస్తుంది", + "description": "వివరణ", + "details": "వివరాలు", + "direction": "దిశ", + "disabled": "నిలిపివేయబడింది", + "disallow_edits": "సవరణలను అనుమతించవద్దు", + "discord": "డిస్కోర్డ్", + "discover": "కనుగొనండి", + "dismiss_all_errors": "అన్ని లోపాలను తీసివేయి", + "dismiss_error": "లోపాన్ని తీసివేయండి", + "display_options": "ప్రదర్శన ఎంపికలు", + "display_order": "ప్రదర్శన క్రమం", + "display_original_photos": "అసలు ఫోటోలను ప్రదర్శించు", + "display_original_photos_setting_description": "ఆస్తిని వీక్షిస్తున్నప్పుడు అసలు ఆస్తి వెబ్-అనుకూలంగా ఉన్నట్టయితే, థంబ్‌నెయిల్‌ల కంటే అసలు ఫోటోను ప్రదర్శించడానికి ఇష్టపడండి. దీని ఫలితంగా ఫోటో ప్రదర్శన వేగం మందగించవచ్చు.", + "do_not_show_again": "ఈ సందేశాన్ని మళ్ళీ చూపించవద్దు", + "documentation": "డాక్యుమెంటేషన్", + "done": "పూర్తయింది", + "download": "డౌన్లోడ్", + "download_include_embedded_motion_videos": "పొందుపరిచిన వీడియోలు", + "download_include_embedded_motion_videos_description": "మోషన్ ఫోటోలలో పొందుపరిచిన వీడియోలను ప్రత్యేక ఫైల్‌గా చేర్చండి", + "download_settings": "డౌన్లోడ్", + "download_settings_description": "ఆస్తి డౌన్‌లోడ్‌కు సంబంధించిన సెట్టింగ్‌లను నిర్వహించండి", + "downloading": "డౌన్‌లోడ్ చేస్తోంది", + "downloading_asset_filename": "ఆస్తి {filename}ని డౌన్‌లోడ్ చేస్తోంది", + "drop_files_to_upload": "అప్‌లోడ్ చేయడానికి ఫైల్‌లను ఎక్కడైనా వదలండి", + "duplicates": "నకిలీలు", + "duplicates_description": "ప్రతి సమూహాన్ని నకిలీలు అని సూచించడం ద్వారా పరిష్కరించండి", + "duration": "వ్యవధి", + "edit": "సవరించు", + "edit_album": "ఆల్బమ్‌ను సవరించు", + "edit_avatar": "అవతార్‌ను సవరించు", + "edit_date": "తేదీని సవరించు", + "edit_date_and_time": "తేదీ మరియు సమయాన్ని సవరించు", + "edit_exclusion_pattern": "మినహాయింపు నమూనాను సవరించు", + "edit_faces": "ముఖాలను సవరించు", + "edit_import_path": "దిగుమతి మార్గాన్ని సవరించు", + "edit_import_paths": "దిగుమతి మార్గాలను సవరించు", + "edit_key": "కీని సవరించు", + "edit_link": "లింక్‌ను సవరించు", + "edit_location": "స్థానాన్ని సవరించు", + "edit_name": "పేరును సవరించు", + "edit_people": "వ్యక్తులను సవరించండి", + "edit_tag": "ట్యాగ్‌ను సవరించు", + "edit_title": "శీర్షికను సవరించు", + "edit_user": "వినియోగదారుని సవరించు", + "edited": "సవరించబడింది", + "editor": "ఎడిటర్", + "editor_close_without_save_prompt": "మార్పులు సేవ్ చేయబడవు", + "editor_close_without_save_title": "ఎడిటర్‌ను మూసివేయాలా?", + "editor_crop_tool_h2_aspect_ratios": "కారక నిష్పత్తులు", + "editor_crop_tool_h2_rotation": "భ్రమణం", + "email": "ఇ-మెయిల్", + "empty_trash": "చెత్తను ఖాళీ చేయి", + "empty_trash_confirmation": "మీరు ఖచ్చితంగా ట్రాష్‌ను ఖాళీ చేయాలనుకుంటున్నారా? ఇది ట్రాష్‌లోని అన్ని ఆస్తులను ఇమ్మిచ్ నుండి శాశ్వతంగా తొలగిస్తుంది.\nమీరు ఈ చర్యను రద్దు చేయలేరు!", + "enable": "ప్రారంభించు", + "enabled": "ప్రారంభించబడింది", + "end_date": "ముగింపు తేదీ", + "error": "లోపం", + "error_delete_face": "ఆస్తి నుండి ముఖాన్ని తొలగించడంలో లోపం ఏర్పడింది", + "error_loading_image": "చిత్రాన్ని లోడ్ చేయడంలో లోపం ఏర్పడింది", + "error_title": "లోపం - ఏదో తప్పు జరిగింది", + "errors": { + "cannot_navigate_next_asset": "తదుపరి ఆస్తికి నావిగేట్ చేయలేరు", + "cannot_navigate_previous_asset": "మునుపటి ఆస్తికి నావిగేట్ చేయలేరు", + "cant_apply_changes": "మార్పులను వర్తింపజేయడం సాధ్యం కాదు", + "cant_change_activity": "ఇతర యాక్టివిటీని {enabled, select, true {disable} other {enable}} చేయలేరు", + "cant_change_asset_favorite": "ఆస్తికి ఇష్టమైనదాన్ని మార్చలేరు", + "cant_change_metadata_assets_count": "{count, plural, one {# asset} other {# assets}} యొక్క మెటాడేటాను మార్చలేము", + "cant_get_faces": "ముఖాలను పొందలేకపోతున్నాను", + "cant_get_number_of_comments": "వ్యాఖ్యల సంఖ్యను పొందలేకపోతున్నాను", + "cant_search_people": "వ్యక్తులను శోధించడం సాధ్యం కాదు", + "cant_search_places": "స్థలాలను శోధించడం సాధ్యం కాలేదు", + "cleared_jobs": "{job} కోసం ఉద్యోగాలను తొలగించారు", + "error_adding_assets_to_album": "ఆల్బమ్‌కు ఆస్తులను జోడించడంలో లోపం ఏర్పడింది", + "error_adding_users_to_album": "ఆల్బమ్‌కు వినియోగదారులను జోడించడంలో లోపం ఏర్పడింది", + "error_deleting_shared_user": "షేర్డ్ యూజర్‌ను తొలగించడంలో ఎర్రర్ ఏర్పడింది", + "error_downloading": "{filename} ని డౌన్‌లోడ్ చేయడంలో లోపం ఏర్పడింది", + "error_hiding_buy_button": "కొనుగోలు బటన్‌ను దాచడంలో లోపం ఏర్పడింది", + "error_removing_assets_from_album": "ఆల్బమ్ నుండి ఆస్తులను తీసివేయడంలో లోపం ఏర్పడింది, మరిన్ని వివరాల కోసం కన్సోల్‌ని తనిఖీ చేయండి", + "error_selecting_all_assets": "అన్ని ఆస్తులను ఎంచుకోవడంలో లోపం ఏర్పడింది", + "exclusion_pattern_already_exists": "ఈ మినహాయింపు నమూనా ఇప్పటికే ఉంది.", + "failed_job_command": "జాబ్ కోసం కమాండ్ {command} విఫలమైంది: {job}", + "failed_to_create_album": "ఆల్బమ్‌ను సృష్టించడంలో విఫలమైంది", + "failed_to_create_shared_link": "షేర్ చేయాల్సిన లింక్‌ను సృష్టించడంలో విఫలమైంది", + "failed_to_edit_shared_link": "షేర్ చేయాల్సిన లింక్‌ను సవరించడంలో విఫలమైంది", + "failed_to_get_people": "వ్యక్తులను పొందడంలో విఫలమైంది", + "failed_to_keep_this_delete_others": "ఈ ఆస్తిని ఉంచుకోవడంలో మరియు ఇతర ఆస్తులను తొలగించడంలో విఫలమైంది", + "failed_to_load_asset": "ఆస్తిని లోడ్ చేయడంలో విఫలమైంది", + "failed_to_load_assets": "ఆస్తులను లోడ్ చేయడంలో విఫలమైంది", + "failed_to_load_people": "వ్యక్తులను లోడ్ చేయడంలో విఫలమైంది", + "failed_to_remove_product_key": "ఉత్పత్తి కీని తీసివేయడంలో విఫలమైంది", + "failed_to_stack_assets": "ఆస్తులను పేర్చడంలో విఫలమైంది", + "failed_to_unstack_assets": "ఆస్తులను అన్-స్టాక్ చేయడంలో విఫలమైంది", + "import_path_already_exists": "ఈ దిగుమతి మార్గం ఇప్పటికే ఉంది.", + "incorrect_email_or_password": "తప్పు ఇమెయిల్ లేదా పాస్‌వర్డ్", + "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} ధ్రువీకరణ విఫలమైంది", + "profile_picture_transparent_pixels": "ప్రొఫైల్ చిత్రాలలో పారదర్శక పిక్సెల్‌లు ఉండకూడదు. దయచేసి చిత్రాన్ని జూమ్ చేయండి మరియు/లేదా తరలించండి.", + "quota_higher_than_disk_size": "మీరు డిస్క్ పరిమాణం కంటే ఎక్కువ కోటాను సెట్ చేసారు", + "repair_unable_to_check_items": "{count, select, one {item} other {items}} ని చెక్ చేయడం సాధ్యం కాలేదు", + "unable_to_add_album_users": "ఆల్బమ్‌కు వినియోగదారులను జోడించడం సాధ్యం కాలేదు", + "unable_to_add_assets_to_shared_link": "షేర్ చేసిన లింక్‌కు ఆస్తులను జోడించడం సాధ్యం కాలేదు", + "unable_to_add_comment": "వ్యాఖ్యను జోడించడం సాధ్యం కాలేదు", + "unable_to_add_exclusion_pattern": "మినహాయింపు నమూనాను జోడించడం సాధ్యం కాలేదు", + "unable_to_add_import_path": "దిగుమతి మార్గాన్ని జోడించడం సాధ్యం కాలేదు", + "unable_to_add_partners": "భాగస్వాములను జోడించడం సాధ్యం కాలేదు", + "unable_to_add_remove_archive": "ఆర్కైవ్ చేయడం {archived, select, true {remove asset from} other {add asset to}} సాధ్యం కాలేదు", + "unable_to_add_remove_favorites": "ఇష్టమైనవిగా {favorite, select, true {add asset to} other {remove asset from}} సాధ్యం కాలేదు", + "unable_to_archive_unarchive": "{archived, select, true {archive} other {unarchive}} సాధ్యం కాలేదు", + "unable_to_change_album_user_role": "ఆల్బమ్ వినియోగదారు పాత్రను మార్చలేకపోయాము", + "unable_to_change_date": "తేదీని మార్చడం సాధ్యం కాలేదు", + "unable_to_change_favorite": "ఆస్తికి ఇష్టమైనదాన్ని మార్చడం సాధ్యం కాలేదు", + "unable_to_change_location": "స్థానాన్ని మార్చడం సాధ్యం కాలేదు", + "unable_to_change_password": "పాస్‌వర్డ్‌ను మార్చలేకపోయాం", + "unable_to_change_visibility": "{count, plural, one {# person} other {# people}} కి దృశ్యమానతను మార్చలేకపోయాము", + "unable_to_complete_oauth_login": "OAuth లాగిన్‌ను పూర్తి చేయడం సాధ్యం కాలేదు", + "unable_to_connect": "కనెక్ట్ చేయడం సాధ్యం కాలేదు", + "unable_to_connect_to_server": "సర్వర్‌కు కనెక్ట్ చేయడం సాధ్యం కాలేదు", + "unable_to_copy_to_clipboard": "క్లిప్‌బోర్డ్‌కు కాపీ చేయడం సాధ్యం కాదు, మీరు https ద్వారా పేజీని యాక్సెస్ చేస్తున్నారని నిర్ధారించుకోండి", + "unable_to_create_admin_account": "నిర్వాహక ఖాతాను సృష్టించడం సాధ్యం కాలేదు", + "unable_to_create_api_key": "కొత్త API కీని సృష్టించడం సాధ్యం కాలేదు", + "unable_to_create_library": "లైబ్రరీని సృష్టించడం సాధ్యం కాలేదు", + "unable_to_create_user": "వినియోగదారుని సృష్టించలేకపోయింది", + "unable_to_delete_album": "ఆల్బమ్‌ను తొలగించడం సాధ్యం కాలేదు", + "unable_to_delete_asset": "ఆస్తిని తొలగించడం సాధ్యం కాలేదు", + "unable_to_delete_assets": "ఆస్తులను తొలగించడంలో లోపం ఏర్పడింది", + "unable_to_delete_exclusion_pattern": "మినహాయింపు నమూనాను తొలగించడం సాధ్యం కాలేదు", + "unable_to_delete_import_path": "దిగుమతి మార్గాన్ని తొలగించలేకపోయింది", + "unable_to_delete_shared_link": "షేర్ చేసిన లింక్‌ను తొలగించడం సాధ్యం కాలేదు", + "unable_to_delete_user": "వినియోగదారుని తొలగించడం సాధ్యం కాలేదు", + "unable_to_download_files": "ఫైళ్లను డౌన్‌లోడ్ చేయడం సాధ్యం కాలేదు", + "unable_to_edit_exclusion_pattern": "మినహాయింపు నమూనాను సవరించడం సాధ్యం కాలేదు", + "unable_to_edit_import_path": "దిగుమతి మార్గాన్ని సవరించడం సాధ్యం కాలేదు", + "unable_to_empty_trash": "ట్రాష్‌ను ఖాళీ చేయడం సాధ్యం కాలేదు", + "unable_to_enter_fullscreen": "పూర్తి స్క్రీన్‌లోకి ప్రవేశించడం సాధ్యం కాలేదు", + "unable_to_exit_fullscreen": "పూర్తి స్క్రీన్ నుండి నిష్క్రమించడం సాధ్యం కాలేదు", + "unable_to_get_comments_number": "వ్యాఖ్యల సంఖ్యను పొందలేకపోయింది", + "unable_to_get_shared_link": "షేర్ చేసిన లింక్‌ను పొందడం విఫలమైంది", + "unable_to_hide_person": "వ్యక్తిని దాచలేకపోయారు", + "unable_to_link_motion_video": "మోషన్ వీడియోను లింక్ చేయడం సాధ్యం కాలేదు", + "unable_to_link_oauth_account": "OAuth ఖాతాను లింక్ చేయడం సాధ్యం కాలేదు", + "unable_to_load_album": "ఆల్బమ్‌ను లోడ్ చేయడం సాధ్యం కాలేదు", + "unable_to_load_asset_activity": "ఆస్తి కార్యాచరణను లోడ్ చేయడం సాధ్యం కాలేదు", + "unable_to_load_items": "అంశాలను లోడ్ చేయడం సాధ్యం కాలేదు", + "unable_to_load_liked_status": "లైక్ చేసిన స్థితిని లోడ్ చేయడం సాధ్యం కాలేదు", + "unable_to_log_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేయడం సాధ్యం కాలేదు", + "unable_to_log_out_device": "పరికరం లాగ్ అవుట్ చేయడం సాధ్యం కాలేదు", + "unable_to_login_with_oauth": "OAuth తో లాగిన్ అవ్వడం సాధ్యం కాలేదు", + "unable_to_play_video": "వీడియో ప్లే చేయడం సాధ్యం కాలేదు", + "unable_to_reassign_assets_existing_person": "{name, select, null {an existing person} other {{name}}}కు ఆస్తులను తిరిగి కేటాయించడం సాధ్యం కాలేదు", + "unable_to_reassign_assets_new_person": "కొత్త వ్యక్తికి ఆస్తులను తిరిగి కేటాయించడం సాధ్యం కాలేదు", + "unable_to_refresh_user": "వినియోగదారుని రిఫ్రెష్ చేయడం సాధ్యం కాలేదు", + "unable_to_remove_album_users": "ఆల్బమ్ నుండి వినియోగదారులను తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_api_key": "API కీని తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_assets_from_shared_link": "షేర్ చేసిన లింక్ నుండి ఆస్తులను తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_deleted_assets": "ఆఫ్‌లైన్ ఫైల్‌లను తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_library": "లైబ్రరీని తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_partner": "భాగస్వామిని తీసివేయడం సాధ్యం కాలేదు", + "unable_to_remove_reaction": "ప్రతిస్పందనను తీసివేయడం సాధ్యం కాలేదు", + "unable_to_repair_items": "వస్తువులను రిపేర్ చేయడం సాధ్యం కాలేదు", + "unable_to_reset_password": "పాస్‌వర్డ్‌ను రీసెట్ చేయడం సాధ్యం కాలేదు", + "unable_to_resolve_duplicate": "నకిలీని పరిష్కరించలేకపోయింది", + "unable_to_restore_assets": "ఆస్తులను పునరుద్ధరించడం సాధ్యం కాలేదు", + "unable_to_restore_trash": "ట్రాష్‌ను పునరుద్ధరించడం సాధ్యం కాలేదు", + "unable_to_restore_user": "వినియోగదారుని పునరుద్ధరించడం సాధ్యం కాలేదు", + "unable_to_save_album": "ఆల్బమ్‌ను సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_save_api_key": "API కీని సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_save_date_of_birth": "పుట్టిన తేదీని సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_save_name": "పేరును సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_save_profile": "ప్రొఫైల్‌ను సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_save_settings": "సెట్టింగ్‌లను సేవ్ చేయడం సాధ్యం కాలేదు", + "unable_to_scan_libraries": "లైబ్రరీలను స్కాన్ చేయడం సాధ్యం కాలేదు", + "unable_to_scan_library": "లైబ్రరీని స్కాన్ చేయడం సాధ్యం కాలేదు", + "unable_to_set_feature_photo": "ఫీచర్ ఫోటోను సెట్ చేయడం సాధ్యం కాలేదు", + "unable_to_set_profile_picture": "ప్రొఫైల్ చిత్రాన్ని సెట్ చేయడం సాధ్యం కాలేదు", + "unable_to_submit_job": "ఉద్యోగాన్ని సమర్పించలేకపోయింది", + "unable_to_trash_asset": "ఆస్తిని ట్రాష్ చేయడం సాధ్యం కాలేదు", + "unable_to_unlink_account": "ఖాతాను అన్‌లింక్ చేయడం సాధ్యం కాలేదు", + "unable_to_unlink_motion_video": "మోషన్ వీడియోను అన్‌లింక్ చేయడం సాధ్యం కాలేదు", + "unable_to_update_album_cover": "ఆల్బమ్ కవర్‌ను నవీకరించలేకపోయింది", + "unable_to_update_album_info": "ఆల్బమ్ సమాచారాన్ని నవీకరించలేకపోయింది", + "unable_to_update_library": "లైబ్రరీని నవీకరించడం సాధ్యం కాలేదు", + "unable_to_update_location": "స్థానాన్ని నవీకరించడం సాధ్యం కాలేదు", + "unable_to_update_settings": "సెట్టింగ్‌లను నవీకరించడం సాధ్యం కాలేదు", + "unable_to_update_timeline_display_status": "టైమ్‌లైన్ డిస్‌ప్లే స్థితిని నవీకరించడం సాధ్యం కాలేదు", + "unable_to_update_user": "వినియోగదారుని నవీకరించలేకపోయింది", + "unable_to_upload_file": "ఫైల్‌ను అప్‌లోడ్ చేయడం సాధ్యం కాలేదు" + }, + "exif": "ఏక్సిఫ్", + "exit_slideshow": "స్లయిడ్ షో నుండి నిష్క్రమించు", + "expand_all": "అన్నీ విస్తరించు", + "expire_after": "దీని తర్వాత గడువు ముగుస్తుంది", + "expired": "గడువు ముగిసింది", + "expires_date": "{date}న గడువు ముగుస్తుంది", + "explore": "అన్వేషించండి", + "explorer": "అన్వేషకుడు", + "export": "ఎగుమతి", + "export_as_json": "JSONగా ఎగుమతి చేయి", + "extension": "పొడిగింపు", + "external": "బాహ్య", + "external_libraries": "బాహ్య గ్రంథాలయాలు", + "face_unassigned": "కేటాయించబడలేదు", + "failed_to_load_assets": "ఆస్తులను లోడ్ చేయడంలో విఫలమైంది", + "favorite": "ఇష్టమైనది", + "favorite_or_unfavorite_photo": "ఇష్టమైన లేదా ఇష్టమైన ఫోటో నుండి తీసివేయి", + "favorites": "ఇష్టమైనవి", + "feature_photo_updated": "ఫీచర్ ఫోటో నవీకరించబడింది", + "features": "లక్షణాలు", + "features_setting_description": "యాప్ ఫీచర్‌లను నిర్వహించండి", + "file_name": "ఫైల్ పేరు", + "file_name_or_extension": "ఫైల్ పేరు లేదా పొడిగింపు", + "filename": "ఫైలుపేరు", + "filetype": "ఫైల్ రకం", + "filter_people": "వ్యక్తులను ఫిల్టర్ చేయండి", + "find_them_fast": "పేరుతో శోధన ద్వారా వాటిని వేగంగా కనుగొనండి", + "fix_incorrect_match": "తప్పు సరిపోలికను పరిష్కరించండి", + "folders": "ఫోల్డర్లు", + "folders_feature_description": "ఫైల్ సిస్టమ్‌లోని ఫోటోలు మరియు వీడియోల కోసం ఫోల్డర్ వీక్షణను బ్రౌజ్ చేయడం", + "forward": "ముందుకు", + "general": "సాధారణ", + "get_help": "సహాయం పొందండి", + "getting_started": "మొదలు అవుతుంది", + "go_back": "వెనక్కి వెళ్ళు", + "go_to_folder": "ఫోల్డర్‌కు వెళ్లండి", + "go_to_search": "శోధనకు వెళ్లండి", + "group_albums_by": "ఆల్బమ్‌లను దీని ద్వారా సమూహపరచండి...", + "group_country": "దేశం వారీగా సమూహం", + "group_no": "గ్రూపింగ్ లేదు", + "group_owner": "యజమాని వారీగా సమూహం చేయండి", + "group_places_by": "స్థలాల ద్వారా సమూహపరచండి...", + "group_year": "సంవత్సరం వారీగా సమూహం చేయండి", + "has_quota": "కోటా ఉంది", + "hi_user": "హాయ్ {name} ({email})", + "hide_all_people": "అందరినీ దాచు", + "hide_gallery": "గ్యాలరీని దాచు", + "hide_named_person": "{name} వ్యక్తిని దాచు", + "hide_password": "పాస్‌వర్డ్‌ను దాచు", + "hide_person": "వ్యక్తిని దాచు", + "hide_unnamed_people": "పేరులేని వ్యక్తులను దాచు", + "host": "హోస్ట్", + "hour": "గంట", + "image": "చిత్రం", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} {date}న తీయబడింది", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} {person1} తో {date}న తీయబడింది", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} {person1} మరియు {person2} తో {date}న తీయబడింది", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} {person1}, {person2}, మరియు {person3} తో {date}న తీయబడింది", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} {person1}, {person2}, మరియు {additionalCount, number} others తో తీసినది {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} {date}న {city}, {country}లో తీయబడింది", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} {date}న {person1} తో {city}, {country} లో తీయబడింది", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} {date}న {person1} మరియు {person2}తో {city}, {country}లో తీయబడింది", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} {city}, {country} లో {person1}, {person2}, మరియు {person3} లతో {date}న తీయబడింది", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} {city}, {country} లో {person1}, {person2}, మరియు {additionalCount, number} others తో {date}న తీయబడింది", + "immich_logo": "ఇమ్మిచ్ లోగో", + "immich_web_interface": "ఇమ్మిచ్ వెబ్ ఇంటర్‌ఫేస్", + "import_from_json": "JSON నుండి దిగుమతి చేయండి", + "import_path": "దిగుమతి మార్గం", + "in_albums": "{count, plural, one {# album} other {# album}} లో", + "in_archive": "ఆర్కైవ్‌లో ఉంది", + "include_archived": "ఆర్కైవ్ చేసిన వాటిని చేర్చు", + "include_shared_albums": "షేర్ చేసిన ఆల్బమ్‌లను చేర్చండి", + "include_shared_partner_assets": "భాగస్వామ్య భాగస్వామి ఆస్తులను చేర్చండి", + "individual_share": "వ్యక్తిగత వాటా", + "individual_shares": "వ్యక్తిగత షేర్లు", + "info": "సమాచారం", + "interval": { + "day_at_onepm": "ప్రతిరోజు మధ్యాహ్నం 1 గంటలకు", + "hours": "ప్రతి {hours, plural, one {hour} other {{hours, number} గంటలు}}", + "night_at_midnight": "ప్రతి రాత్రి అర్ధరాత్రి", + "night_at_twoam": "ప్రతి రాత్రి 2 గంటలకు" + }, + "invite_people": "వ్యక్తులను ఆహ్వానించండి", "invite_to_album": "ఆల్బమ్‌కు ఆహ్వానించండి", + "items_count": "{count, plural, one {# అంశం} other {# అంశాలు}}", "jobs": "ఉద్యోగాలు", "keep": "ఉంచండి", "keep_all": "అన్ని ఉంచు", + "keep_this_delete_others": "దీన్ని ఉంచు, మిగతా వాటిని తొలగించు", + "kept_this_deleted_others": "ఈ ఆస్తిని ఉంచుకుని తొలగించారు {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "కీబోర్డ్ సత్వరమార్గాలు", "language": "భాష", "language_setting_description": "మీకు ఇష్టమైన భాషను ఎంచుకోండి", "last_seen": "ఆఖరి సారిగా చూచింది", + "latest_version": "తాజా వెర్షన్", "latitude": "అక్షాంశం", "leave": "వదిలేయ్", + "lens_model": "లెన్స్ మోడల్", "let_others_respond": "ఇతరులు ప్రతిస్పందించనివ్వండి", "level": "స్థాయి", "library": "గ్రంధాలయం", "library_options": "లైబ్రరీ ఎంపికలు", "light": "వెలుతురు", + "like_deleted": "ఇస్టం తొలగించబడింది", + "link_motion_video": "మోషన్ వీడియో లింక్ చేయండి", "link_options": "లింక్ ఎంపికలు", + "link_to_oauth": "OAuth కి లింక్ చేయండి", "linked_oauth_account": "లింక్ చేయబడిన OAuth ఖాతా", "list": "జాబితా", "loading": "లోడ్", @@ -162,18 +850,25 @@ "log_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేయండి", "logged_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేసారు", "logged_out_device": "పరికరం లాగ్ అవుట్ చేయబడింది", + "login": "లాగిన్", + "login_has_been_disabled": "లాగిన్ నిలిపివేయబడింది.", + "logout_all_device_confirmation": "మీరు ఖచ్చితంగా అన్ని పరికరాల నుండి లాగ్ అవుట్ చేయాలనుకుంటున్నారా?", "logout_this_device_confirmation": "మీరు ఖచ్చితంగా ఈ పరికరాన్ని లాగ్ అవుట్ చేయాలనుకుంటున్నారా?", "longitude": "రేఖాంశం", "look": "చూడు", "loop_videos": "లూప్ వీడియోలు", "loop_videos_description": "వివరాల వ్యూయర్‌లో వీడియోను స్వయంచాలకంగా లూప్ చేయడానికి ప్రారంభించండి.", + "main_branch_warning": "మీరు డెవలప్‌మెంట్ వెర్షన్‌ను ఉపయోగిస్తున్నారు; విడుదల వెర్షన్‌ను ఉపయోగించమని మేము గట్టిగా సిఫార్సు చేస్తున్నాము!", "make": "తయారు చేయండి", "manage_shared_links": "భాగస్వామ్య లింక్‌లను నిర్వహించండి", "manage_sharing_with_partners": "భాగస్వాములతో భాగస్వామ్యాన్ని నిర్వహించండి", "manage_the_app_settings": "యాప్ సెట్టింగ్‌లను నిర్వహించండి", "manage_your_account": "మీ ఖాతా నిర్వహించుకొనండి", + "manage_your_api_keys": "మీ API కీలను నిర్వహించండి", + "manage_your_devices": "మీ లాగిన్ అయిన పరికరాలను నిర్వహించండి", "manage_your_oauth_connection": "మీ OAuth కనెక్షన్‌ని నిర్వహించండి", "map": "మ్యాప్", + "map_marker_for_images": "{city}, {country} లో తీసిన చిత్రాల కోసం మ్యాప్ మార్కర్", "map_marker_with_image": "చిత్రంతో మ్యాప్ మార్కర్", "map_settings": "మ్యాప్ సెట్టింగ్‌లు", "matches": "మ్యాచ్‌లు", @@ -181,12 +876,14 @@ "memories": "జ్ఞాపకాలు", "memories_setting_description": "మీ జ్ఞాపకాలలో మీరు చూసే వాటిని నిర్వహించండి", "memory": "గ్నాపకం", + "memory_lane_title": "మెమరీ లేన్ {title }", "menu": "మెను", "merge": "విలీనం", "merge_people": "వ్యక్తులను విలీనం చేయండి", "merge_people_limit": "మీరు ఒకేసారి 5 ముఖాలను మాత్రమే విలీనం చేయగలరు", "merge_people_prompt": "మీరు ఈ వ్యక్తులను విలీనం చేయాలనుకుంటున్నారా? ఈ చర్య తిరుగులేనిది.", "merge_people_successfully": "వ్యక్తులను విజయవంతంగా విలీనం చేసారు", + "merged_people_count": "విలీనం చేయబడింది {count, plural, one {# person} other {# people}}", "minimize": "తగ్గించండి", "minute": "నిమిషం", "missing": "తప్పిపోయింది", @@ -194,14 +891,17 @@ "month": "నెల", "more": "మరింత", "moved_to_trash": "ట్రాష్‌కి తరలించబడింది", + "mute_memories": "జ్ఞాపకాలను మ్యూట్ చేయి", "my_albums": "నా ఆల్బమ్‌లు", "name": "పేరు", "name_or_nickname": "పేరు లేదా మారుపేరు", "never": "ఎప్పుడు కాదు", "new_album": "కొత్త ఆల్బమ్", + "new_api_key": "కొత్త API కీ", "new_password": "కొత్త పాస్వర్డ్", "new_person": "కొత్త వ్యక్తి", "new_user_created": "కొత్త వినియోగదారి సృష్టించబడ్డారు", + "new_version_available": "కొత్త వెర్షన్ అందుబాటులో ఉంది", "newest_first": "మొదటిది సరికొత్తది", "next": "తరువాత", "next_memory": "తదుపరి జ్ఞాపకం", @@ -212,6 +912,7 @@ "no_archived_assets_message": "మీ ఫోటోల వీక్షణ నుండి వాటిని దాచడానికి ఫోటోలు మరియు వీడియోలను ఆర్కైవ్ చేయండి", "no_assets_message": "మీ మొదటి ఫోటోను అప్‌లోడ్ చేయడానికి క్లిక్ చేయండి", "no_duplicates_found": "నకిలీలు ఏవీ కనుగొనబడలేదు.", + "no_exif_info_available": "ఎక్సిఫ్ సమాచారం అందుబాటులో లేదు", "no_explore_results_message": "మీ సేకరణను అన్వేషించడానికి మరిన్ని ఫోటోలను అప్‌లోడ్ చేయండి.", "no_favorites_message": "మీ ఉత్తమ చిత్రాలు మరియు వీడియోలను త్వరగా కనుగొనడానికి ఇష్టమైన వాటిని జోడించండి", "no_libraries_message": "మీ ఫోటోలు మరియు వీడియోలను వీక్షించడానికి బాహ్య లైబ్రరీని సృష్టించండి", @@ -221,13 +922,251 @@ "no_results_description": "పర్యాయపదం లేదా మరింత సాధారణ కీవర్డ్‌ని ప్రయత్నించండి", "no_shared_albums_message": "మీ నెట్‌వర్క్‌లోని వ్యక్తులతో ఫోటోలు మరియు వీడియోలను భాగస్వామ్యం చేయడానికి ఆల్బమ్‌ను సృష్టించండి", "not_in_any_album": "ఏ ఆల్బమ్‌లోనూ లేదు", + "note_apply_storage_label_to_previously_uploaded assets": "గమనిక: గతంలో అప్‌లోడ్ చేసిన ఆస్తులకు నిల్వ లేబుల్‌ను వర్తింపజేయడానికి,", "note_unlimited_quota": "గమనిక: అపరిమిత కోటా కోసం 0ని నమోదు చేయండి", "notes": "గమనికలు", "notification_toggle_setting_description": "ఇమెయిల్ నోటిఫికేషన్‌లను ప్రారంభించండి", "notifications": "నోటిఫికేషన్‌లు", "notifications_setting_description": "నోటిఫికేషన్‌లను నిర్వహించండి", "oauth": "OAuth", + "official_immich_resources": "అధికారిక ఇమ్మిచ్ వనరులు", + "offline": "ఆఫ్‌లైన్", + "offline_paths": "ఆఫ్‌లైన్ పాత్‌లు", + "offline_paths_description": "బాహ్య లైబ్రరీలో భాగం కాని ఫైళ్ళను మాన్యువల్‌గా తొలగించడం వల్ల ఈ ఫలితాలు వచ్చి ఉండవచ్చు.", + "ok": "సరే", + "oldest_first": "ముందుగా పాతది", + "onboarding": "ఆన్‌బోర్డింగ్", + "onboarding_privacy_description": "కింది (ఐచ్ఛికం) లక్షణాలు బాహ్య సేవలపై ఆధారపడి ఉంటాయి మరియు పరిపాలన సెట్టింగ్‌లలో ఎప్పుడైనా నిలిపివేయబడతాయి.", + "onboarding_theme_description": "మీ ఇన్స్టాన్స్‌ కోసం రంగు థీమ్‌ను ఎంచుకోండి. మీరు దీన్ని తర్వాత మీ సెట్టింగ్‌లలో మార్చవచ్చు.", + "onboarding_welcome_description": "మీ ఇన్స్టాన్స్‌ను కొన్ని సాధారణ సెట్టింగ్‌లతో సెటప్ చేసుకుందాం.", + "onboarding_welcome_user": "స్వాగతం, {user }", + "online": "ఆన్‌లైన్", + "only_favorites": "ఇష్టమైనవి మాత్రమే", + "open_in_map_view": "మ్యాప్ వీక్షణలో తెరవండి", + "open_in_openstreetmap": "ఓపెన్‌స్ట్రీట్‌మ్యాప్‌లో తెరవండి", + "open_the_search_filters": "శోధన ఫిల్టర్‌లను తెరవండి", + "options": "ఎంపికలు", + "or": "లేదా", + "organize_your_library": "మీ లైబ్రరీని నిర్వహించండి", + "original": "అసలు", + "other": "ఇతర", + "other_devices": "ఇతర పరికరాలు", + "other_variables": "ఇతర వేరియబుల్స్", + "owned": "స్వంతం చేసుకున్నవి", + "owner": "యజమాని", + "partner": "భాగస్వామి", + "partner_can_access": "{partner} యాక్సెస్ చేయగలరు", + "partner_can_access_assets": "ఆర్కైవ్ చేయబడినవి మరియు తొలగించబడినవి తప్ప మీ అన్ని ఫోటోలు మరియు వీడియోలు", + "partner_can_access_location": "మీ ఫోటోలు తీసిన ప్రదేశం", + "partner_sharing": "భాగస్వామి భాగస్వామ్యం", + "partners": "భాగస్వాములు", + "password": "పాస్‌వర్డ్", + "password_does_not_match": "పాస్‌వర్డ్ సరిపోలలేదు", + "password_required": "పాస్‌వర్డ్ అవసరం", + "password_reset_success": "పాస్‌వర్డ్ రీసెట్ విజయవంతమైంది", + "past_durations": { + "days": "గత {days, plural, one {day} other {# days}}", + "hours": "గత {hours, plural, one {hour} other {# hours}}", + "years": "గత {years, plural, one {year} other {# years}}" + }, + "path": "మార్గం", + "pattern": "నమూనా", + "pause": "పాజ్ చేయండి", + "pause_memories": "జ్ఞాపకాలను పాజ్ చేయి", + "paused": "పాజ్ చేయబడింది", + "pending": "పెండింగ్‌లో ఉంది", + "people": "ప్రజలు", + "people_edits_count": "సవరించబడింది {count, plural, one {# person} other {# people}}", + "people_feature_description": "వ్యక్తుల వారీగా సమూహపరచబడిన ఫోటోలు మరియు వీడియోలను బ్రౌజ్ చేయడం", + "people_sidebar_description": "సైడ్‌బార్‌లో వ్యక్తులకు లింక్‌ను ప్రదర్శించండి", + "permanent_deletion_warning": "శాశ్వత తొలగింపు హెచ్చరిక", + "permanent_deletion_warning_setting_description": "ఆస్తులను శాశ్వతంగా తొలగిస్తున్నప్పుడు హెచ్చరికను చూపించు", + "permanently_delete": "శాశ్వతంగా తొలగించు", + "permanently_delete_assets_count": "శాశ్వతంగా తొలగించు {count, plural, one {asset} other {assets}}", + "permanently_delete_assets_prompt": "మీరు {count, plural, one {this asset?} other {these # assets?}}లను శాశ్వతంగా తొలగించాలనుకుంటున్నారా? ఇది {count, plural, one {it from its} other {them from their}} ఆల్బమ్(లు) ను కూడా తొలగిస్తుంది.", + "permanently_deleted_asset": "శాశ్వతంగా తొలగించబడిన ఆస్తి", + "permanently_deleted_assets_count": "శాశ్వతంగా తొలగించబడింది {count, plural, one {# asset} other {# assets}}", + "person": "వ్యక్తి", + "person_birthdate": "{date}న జన్మించారు", + "person_hidden": "{name}{hidden, select, true { (దాయబడింది)} other {}}", + "photo_shared_all_users": "మీరు మీ ఫోటోలను అందరు వినియోగదారులతో పంచుకున్నట్లు కనిపిస్తోంది లేదా మీకు షేర్ చేయడానికి ఎవరూ లేరు.", + "photos": "ఫోటోలు", + "photos_and_videos": "ఫోటోలు & వీడియోలు", + "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} ఫోటోలు}}", + "photos_from_previous_years": "గత సంవత్సరాల ఫోటోలు", + "pick_a_location": "ఒక స్థానాన్ని ఎంచుకోండి", + "place": "స్థలం", + "places": "స్థలాలు", + "play": "ప్లే", + "play_memories": "ప్లే మెమరీస్", + "play_motion_photo": "మోషన్ ఫోటోను ప్లే చేయి", + "play_or_pause_video": "వీడియోను ప్లే చేయండి లేదా పాజ్ చేయండి", + "port": "పోర్ట్", + "preset": "ప్రీసెట్", + "preview": "ప్రివ్యూ", + "previous": "మునుపటి", + "previous_memory": "మునుపటి జ్ఞాపకం", + "previous_or_next_photo": "మునుపటి లేదా తదుపరి ఫోటో", + "primary": "ప్రాథమిక", + "privacy": "గోప్యత", + "profile_image_of_user": "{user} ప్రొఫైల్ ఇమేజ్", + "profile_picture_set": "ప్రొఫైల్ చిత్రం సెట్ చేయబడింది.", + "public_album": "పబ్లిక్ ఆల్బమ్", + "public_share": "పబ్లిక్ షేర్", + "purchase_account_info": "మద్దతుదారు", + "purchase_activated_subtitle": "Immich మరియు ఓపెన్ సోర్స్ సాఫ్ట్‌వేర్‌లకు మద్దతు ఇచ్చినందుకు ధన్యవాదాలు", + "purchase_activated_time": "{date, date}న యాక్టివేట్ చేయబడింది", + "purchase_activated_title": "మీ కీ విజయవంతంగా యాక్టివేట్ చేయబడింది", + "purchase_button_activate": "యాక్టివేట్ చేయండి", + "purchase_button_buy": "కొను", + "purchase_button_buy_immich": "Immich ని కొను", + "purchase_button_never_show_again": "మళ్ళీ ఎప్పుడూ చూపించవద్దు", + "purchase_button_reminder": "30 రోజుల్లో నాకు గుర్తు చేయి", + "purchase_button_remove_key": "కీని తీసివేయండి", + "purchase_button_select": "ఎంచుకోండి", + "purchase_failed_activation": "యాక్టివేట్ చేయడం విఫలమైంది! దయచేసి సరైన ఉత్పత్తి కీ కోసం మీ ఇమెయిల్‌ను తనిఖీ చేయండి!", + "purchase_individual_description_1": "ఒక వ్యక్తి కోసం", + "purchase_individual_description_2": "మద్దతుదారు స్థితి", + "purchase_individual_title": "వ్యక్తిగత", + "purchase_input_suggestion": "ఉత్పత్తి కీ ఉందా? కింద కీని నమోదు చేయండి", + "purchase_license_subtitle": "సేవ యొక్క నిరంతర అభివృద్ధికి మద్దతు ఇవ్వడానికి Immichను కొనుగోలు చేయండి", + "purchase_lifetime_description": "జీవితకాల కొనుగోలు", + "purchase_option_title": "కొనుగోలు ఎంపికలు", + "purchase_panel_info_1": "Immich ను నిర్మించడానికి చాలా సమయం మరియు కృషి అవసరం, మరియు మేము దానిని సాధ్యమైనంత ఉత్తమంగా చేయడానికి పూర్తి సమయం ఇంజనీర్లు దానిపై పనిచేస్తున్నారు. ఓపెన్-సోర్స్ సాఫ్ట్‌వేర్ మరియు నైతిక వ్యాపార పద్ధతులు డెవలపర్‌లకు స్థిరమైన ఆదాయ వనరుగా మారడం మరియు దోపిడీ క్లౌడ్ సేవలకు నిజమైన ప్రత్యామ్నాయాలతో గోప్యతను గౌరవించే పర్యావరణ వ్యవస్థను సృష్టించడం మా లక్ష్యం.", + "purchase_panel_info_2": "మేము పేవాల్‌లను జోడించకూడదని కట్టుబడి ఉన్నందున, ఈ కొనుగోలు మీకు ఇమ్మిచ్‌లో ఎటువంటి అదనపు ఫీచర్‌లను మంజూరు చేయదు. ఇమ్మిచ్ యొక్క కొనసాగుతున్న అభివృద్ధికి మద్దతు ఇవ్వడానికి మేము మీలాంటి వినియోగదారులపై ఆధారపడతాము.", + "purchase_panel_title": "ప్రాజెక్ట్‌కు మద్దతు ఇవ్వండి", + "purchase_per_server": "ప్రతీ సర్వర్‌కు", + "purchase_per_user": "ప్రతి వినియోగదారునికి", + "purchase_remove_product_key": "ఉత్పత్తి కీని తీసివేయండి", + "purchase_remove_product_key_prompt": "మీరు ఖచ్చితంగా ఉత్పత్తి కీని తీసివేయాలనుకుంటున్నారా?", + "purchase_remove_server_product_key": "సర్వర్ ఉత్పత్తి కీని తీసివేయండి", + "purchase_remove_server_product_key_prompt": "మీరు సర్వర్ ఉత్పత్తి కీని ఖచ్చితంగా తీసివేయాలనుకుంటున్నారా?", + "purchase_server_description_1": "మొత్తం సర్వర్ కోసం", + "purchase_server_description_2": "మద్దతుదారు స్థితి", + "purchase_server_title": "సర్వర్", + "purchase_settings_server_activated": "సర్వర్ ఉత్పత్తి కీని నిర్వాహకుడు నిర్వహిస్తారు", + "rating": "స్టార్ రేటింగ్", + "rating_clear": "రేటింగ్‌ను క్లియర్ చేయి", + "rating_description": "సమాచార ప్యానెల్‌లో EXIF రేటింగ్‌ను ప్రదర్శించండి", + "reaction_options": "ప్రతిచర్య ఎంపికలు", + "read_changelog": "చేంజ్‌లాగ్ చదవండి", + "reassign": "తిరిగి కేటాయించు", + "reassing_hint": "ఎంచుకున్న ఆస్తులను ఇప్పటికే ఉన్న వ్యక్తికి కేటాయించండి", + "recent": "ఇటీవలి", + "recent-albums": "ఇటీవలి ఆల్బమ్‌లు", + "recent_searches": "ఇటీవలి శోధనలు", + "refresh": "రిఫ్రెష్ చేయి", + "refresh_encoded_videos": "ఎన్‌కోడ్ చేసిన వీడియోలను రిఫ్రెష్ చేయండి", + "refresh_faces": "ముఖాలను రిఫ్రెష్ చేయి", + "refresh_metadata": "మెటాడేటాను రిఫ్రెష్ చేయి", + "refresh_thumbnails": "థంబ్‌నెయిల్స్‌ను రిఫ్రెష్ చేయి", + "refreshed": "రిఫ్రెష్ చేయబడింది", + "refreshes_every_file": "ఇప్పటికే ఉన్న మరియు కొత్త అన్ని ఫైళ్ళను తిరిగి చదువుతుంది", + "refreshing_encoded_video": "ఎన్కోడ్ చేసిన వీడియోను రిఫ్రెష్ చేస్తోంది", + "refreshing_faces": "ముఖాలను రిఫ్రెష్ చేస్తోంది", + "refreshing_metadata": "మెటాడేటాను రిఫ్రెష్ చేస్తోంది", + "regenerating_thumbnails": "థంబ్‌నెయిల్‌లను పునరుజ్జీవింపజేస్తోంది", + "remove_assets_title": "ఆస్తులను తీసివేయాలా?", + "remove_custom_date_range": "అనుకూల తేదీ పరిధిని తీసివేయండి", + "remove_deleted_assets": "తొలగించబడిన ఆస్తులను తీసివేయండి", + "remove_from_album": "ఆల్బమ్ నుండి తీసివేయి", + "remove_from_favorites": "ఇష్టమైన వాటి నుండి తీసివేయి", + "remove_from_shared_link": "షేర్ చేసిన లింక్ నుండి తీసివేయండి", + "remove_memory": "మెమరీని తీసివేయండి", + "remove_photo_from_memory": "ఈ జ్ఞాపకం నుండి ఫోటోను తీసివేయండి", + "remove_url": "URL ను తొలగించు", + "remove_user": "వినియోగదారుని తీసివేయి", + "removed_api_key": "తొలగించబడిన API కీ: {name}", + "removed_from_archive": "ఆర్కైవ్ నుండి తీసివేయబడింది", + "removed_from_favorites": "ఇష్టమైనవి నుండి తీసివేయబడింది", + "removed_memory": "మెమరీ తీసివేయబడింది", + "removed_photo_from_memory": "మెమరీ నుండి ఫోటో తీసివేయబడింది", + "rename": "పేరు మార్చండి", + "repair": "మరమ్మత్తు", + "repair_no_results_message": "ట్రాక్ చేయని మరియు తప్పిపోయిన ఫైల్‌లు ఇక్కడ కనిపిస్తాయి", + "replace_with_upload": "అప్‌లోడ్‌తో భర్తీ చేయి", + "repository": "రిపోజిటరీ", + "require_password": "పాస్‌వర్డ్ కావాలి", + "require_user_to_change_password_on_first_login": "మొదటి లాగిన్‌లో యూజర్ పాస్‌వర్డ్ మార్చవలసి ఉంటుంది", + "rescan": "మళ్ళీ స్కాన్ చేయి", + "reset": "తిరిగి నిర్దారించు", + "reset_password": "పాస్‌వర్డ్‌ను రీసెట్ చేయండి", + "reset_people_visibility": "వ్యక్తుల దృశ్యమానతను రీసెట్ చేయండి", + "reset_to_default": "డిఫాల్ట్‌కు రీసెట్ చేయి", + "resolve_duplicates": "నకిలీలను పరిష్కరించండి", + "resolved_all_duplicates": "అన్ని నకిలీలను పరిష్కరించారు", + "restore": "పునరుద్ధరించు", + "restore_all": "అన్నీ పునరుద్ధరించు", + "restore_user": "వినియోగదారుని పునరుద్ధరించు", + "restored_asset": "ఆస్తి పునరుద్ధరించబడింది", + "resume": "పునఃప్రారంభం", + "retry_upload": "అప్‌లోడ్‌ను మళ్లీ ప్రయత్నించండి", + "review_duplicates": "నకిలీలను సమీక్షించండి", + "role": "పాత్ర", + "role_editor": "సవరించగలిగేవారు", + "role_viewer": "చూడగలిగేవారు", + "save": "సేవ్", + "saved_api_key": "సేవ్ చేయబడిన API కీ", + "saved_profile": "సేవ్ చేయబడిన ప్రొఫైల్", + "saved_settings": "సేవ్ చేసిన సెట్టింగ్‌లు", + "say_something": "ఏదైనా చెప్పు", + "scan_all_libraries": "అన్ని లైబ్రరీలను స్కాన్ చేయండి", + "scan_library": "స్కాన్ చేయండి", + "scan_settings": "స్కాన్ సెట్టింగ్‌లు", + "scanning_for_album": "ఆల్బమ్ కోసం స్కాన్ చేస్తోంది...", + "search": "వెతకండి", + "search_albums": "ఆల్బమ్‌లను శోధించు", + "search_by_context": "సందర్భం ఆధారంగా శోధించండి", "search_by_description": "వివరణ ద్వారా శోధించండి", + "search_by_description_example": "సాపాలో హైకింగ్ డే", + "search_by_filename": "ఫైల్ పేరు లేదా పొడిగింపు ద్వారా శోధించండి", + "search_by_filename_example": "అంటే IMG_1234.JPG లేదా PNG", + "search_camera_make": "కెమెరా తయారీని శోధించండి...", + "search_camera_model": "కెమెరా మోడల్‌ను శోధించండి...", + "search_city": "నగరాన్ని శోధించు...", + "search_country": "దేశాన్ని శోధించు...", + "search_for": "వెతుకు", + "search_for_existing_person": "ఇప్పటికే ఉన్న వ్యక్తి కోసం శోధించండి", + "search_no_people": "వ్యక్తులు లేరు", + "search_no_people_named": "\"{name}\" అనే పేరు గల వ్యక్తులు లేరు", + "search_options": "శోధన ఎంపికలు", + "search_people": "వ్యక్తులను శోధించు", + "search_places": "స్థలాలను శోధించు", + "search_rating": "రేటింగ్ ద్వారా శోధించండి...", + "search_settings": "శోధన సెట్టింగ్‌లు", + "search_state": "శోధన స్థితి...", + "search_tags": "ట్యాగ్‌లను శోధించు...", + "search_timezone": "సమయ మండలిని శోధించు...", + "search_type": "శోధన రకం", + "search_your_photos": "మీ ఫోటోలను శోధించండి", + "searching_locales": "స్థానిక ప్రదేశాలను శోధిస్తోంది...", + "second": "రెండవది", + "see_all_people": "అందరు వ్యక్తులను చూడండి", + "select": "ఎంచుకోండి", + "select_album_cover": "ఆల్బమ్ కవర్‌ను ఎంచుకోండి", + "select_all": "అన్నీ ఎంచుకోండి", + "select_all_duplicates": "అన్ని నకిలీలను ఎంచుకోండి", + "select_avatar_color": "అవతార్ రంగును ఎంచుకోండి", + "select_face": "ముఖాన్ని ఎంచుకోండి", + "select_featured_photo": "ఫీచర్ చేయబడిన ఫోటోను ఎంచుకోండి", + "select_from_computer": "కంప్యూటర్ నుండి ఎంచుకోండి", + "select_keep_all": "అన్నీ ఉంచు ఎంచుకోండి", + "select_library_owner": "లైబ్రరీ యజమానిని ఎంచుకోండి", + "select_new_face": "కొత్త ముఖాన్ని ఎంచుకోండి", + "select_photos": "ఫోటోలను ఎంచుకోండి", + "select_trash_all": "అన్నీ చెత్తకు ఎంచుకోండి", + "selected": "ఎంచుకున్నారు", + "send_message": "సందేశం పంపండి", + "send_welcome_email": "స్వాగత ఇమెయిల్ పంపండి", + "server_offline": "సర్వర్ ఆఫ్‌లైన్", + "server_online": "సర్వర్ ఆన్‌లైన్", + "server_stats": "సర్వర్ గణాంకాలు", + "server_version": "సర్వర్ వెర్షన్", + "set": "సెట్", + "set_as_album_cover": "ఆల్బమ్ కవర్‌గా సెట్ చేయి", + "set_as_featured_photo": "ఫీచర్ చేయబడిన ఫోటోగా సెట్ చేయి", + "set_as_profile_picture": "ప్రొఫైల్ చిత్రంగా సెట్ చేయి", + "set_date_of_birth": "పుట్టిన తేదీని సెట్ చేయండి", "unsaved_change": "సేవ్ చేయని మార్పు", "unselect_all": "ఎంచుకున్నవన్నీ తొలగించు", "unselect_all_duplicates": "అన్ని నకిలీల ఎంపికను తీసివేయండి", diff --git a/i18n/uk.json b/i18n/uk.json index 6bce9df091..6fabec2ddf 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -29,7 +29,7 @@ "added_to_favorites_count": "Додано {count, number} до обраного", "admin": { "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".", - "asset_offline_description": "Цей зовнішній бібліотечний актив більше не знайдено на диску і був переміщений до кошика. Якщо файл був переміщений у межах бібліотеки, перевірте свій таймлайн на наявність нового відповідного активу. Щоб відновити цей актив, переконайтеся, що шлях файлу нижче доступний для Immich, і проскануйте бібліотеку.", + "asset_offline_description": "Цей зовнішній бібліотечний актив більше не знайдено на диску і був переміщений до смітника. Якщо файл був переміщений у межах бібліотеки, перевірте свій таймлайн на наявність нового відповідного активу. Щоб відновити цей актив, переконайтеся, що шлях файлу нижче доступний для Immich, і проскануйте бібліотеку.", "authentication_settings": "Налаштування аутентифікації", "authentication_settings_description": "Управління паролями, OAuth та іншими налаштуваннями аутентифікації", "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.", @@ -331,11 +331,11 @@ "transcoding_two_pass_encoding_setting_description": "Транскодування за двома проходами для отримання кращих закодованих відео. Коли ввімкнено максимальний бітрейт (необхідний для роботи з H.264 та HEVC), цей режим використовує діапазон бітрейту, заснований на максимальному бітрейті, і ігнорує CRF. Для VP9 можна використовувати CRF, якщо вимкнено максимальний бітрейт.", "transcoding_video_codec": "Відеокодек", "transcoding_video_codec_description": "VP9 має високу ефективність і сумісність з вебом, але потребує більше часу на транскодування. HEVC працює схоже, але має меншу сумісність з вебом. H.264 має широку сумісність і швидко транскодується, але створює значно більші файли. AV1 - найефективніший кодек, але не підтримується на старіших пристроях.", - "trash_enabled_description": "Увімкнення кошика", + "trash_enabled_description": "Увімкнення смітника", "trash_number_of_days": "Кількість днів", - "trash_number_of_days_description": "Кількість днів, щоб залишити ресурси в кошику перед остаточним їх видаленням", - "trash_settings": "Налаштування кошика", - "trash_settings_description": "Керування налаштуваннями кошика", + "trash_number_of_days_description": "Кількість днів, щоб залишити ресурси в смітнику перед остаточним їх видаленням", + "trash_settings": "Налаштування смітника", + "trash_settings_description": "Керування налаштуваннями смітника", "untracked_files": "Невідстежувані файли", "untracked_files_description": "Ці файли не відстежуються програмою. Вони можуть бути результатом невдалого переміщення, перерваного завантаження або залишитися через помилку програми", "user_cleanup_job": "Очищення користувача", @@ -426,12 +426,12 @@ "assets_added_to_album_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} до альбому", "assets_added_to_name_count": "Додано {count, plural, one {# елемент} other {# елементів}} до {hasName, select, true {{name}} other {нового альбому}}", "assets_count": "{count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у кошик", + "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у смітник", "assets_permanently_deleted_count": "Остаточно видалено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_count": "Вилучено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з кошика? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.", + "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з смітника? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.", "assets_restored_count": "Відновлено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_trashed_count": "Поміщено в кошик {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", + "assets_trashed_count": "Поміщено в смітник {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_were_part_of_album_count": "{count, plural, one {Ресурс був} few {Ресурси були} other {Ресурси були}} вже частиною альбому", "authorized_devices": "Авторизовані пристрої", "back": "Назад", @@ -445,7 +445,7 @@ "build_image": "Версія збірки", "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", - "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", + "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в смітник {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в смітник всі інші дублікати.", "buy": "Придбайте Immich", "camera": "Камера", "camera_brand": "Марка камери", @@ -600,8 +600,8 @@ "editor_crop_tool_h2_aspect_ratios": "Пропорції зображення", "editor_crop_tool_h2_rotation": "Орієнтація", "email": "Електронна пошта", - "empty_trash": "Очистити кошик", - "empty_trash_confirmation": "Ви впевнені, що хочете очистити кошик? Це остаточно видалить всі ресурси в кошику з Immich.\nЦю дію не можна скасувати!", + "empty_trash": "Очистити смітник", + "empty_trash_confirmation": "Ви впевнені, що хочете очистити смітник? Це остаточно видалить всі ресурси в смітнику з Immich.\nЦю дію не можна скасувати!", "enable": "Увімкнути", "enabled": "Увімкнено", "end_date": "Дата завершення", @@ -680,7 +680,7 @@ "unable_to_download_files": "Неможливо завантажити файли", "unable_to_edit_exclusion_pattern": "Не вдалося редагувати шаблон виключення", "unable_to_edit_import_path": "Неможливо відредагувати шлях імпорту", - "unable_to_empty_trash": "Неможливо очистити кошик", + "unable_to_empty_trash": "Неможливо очистити смітник", "unable_to_enter_fullscreen": "Неможливо увійти в повноекранний режим", "unable_to_exit_fullscreen": "Неможливо вийти з повноекранного режиму", "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", @@ -710,7 +710,7 @@ "unable_to_reset_password": "Не вдається скинути пароль", "unable_to_resolve_duplicate": "Не вдається вирішити дублікат", "unable_to_restore_assets": "Неможливо відновити активи", - "unable_to_restore_trash": "Неможливо відновити сміття", + "unable_to_restore_trash": "Не вдалося відновити вміст", "unable_to_restore_user": "Не вдається відновити користувача", "unable_to_save_album": "Не вдається зберегти альбом", "unable_to_save_api_key": "Не вдається зберегти ключ API", @@ -1289,7 +1289,7 @@ "toggle_theme": "Перемикання теми", "total": "Усього", "total_usage": "Загальне використання", - "trash": "Кошик", + "trash": "Смітник", "trash_all": "Видалити все", "trash_count": "Видалити {count, number}", "trash_delete_asset": "Смітник/Видалити ресурс", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index bc2fdb88b7..bde73028f8 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -54,7 +54,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update && apt-get install -y --no-install-recommends g++ -COPY --from=ghcr.io/astral-sh/uv:latest@sha256:57da96c4557243fc0a732817854084e81af9393f64dc7d172f39c16465b5e2ba /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:latest@sha256:fb91e82e8643382d5bce074ba0d167677d678faff4bd518dac670476d19b159c /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ @@ -67,7 +67,7 @@ FROM python:3.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf1 ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 -FROM prod-cpu AS prod-openvino +FROM python:3.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf19cd45e6a52d541e6c178f0 AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ @@ -82,6 +82,8 @@ RUN apt-get update && \ FROM nvidia/cuda:12.2.2-runtime-ubuntu22.04@sha256:94c1577b2cd9dd6c0312dc04dff9cb2fdce2b268018abc3d7c2dbcacf1155000 AS prod-cuda +ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 + RUN apt-get update && \ apt-get install --no-install-recommends -yqq libcudnn9-cuda-12 && \ apt-get clean && \ @@ -140,7 +142,6 @@ ENV TRANSFORMERS_CACHE=/cache \ PYTHONPATH=/usr/src \ DEVICE=${DEVICE} \ VIRTUAL_ENV=/opt/venv \ - LD_BIND_NOW=1 \ MACHINE_LEARNING_CACHE_FOLDER=/cache # prevent core dumps @@ -149,6 +150,7 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \ echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile COPY --from=builder /opt/venv /opt/venv +COPY scripts/healthcheck.py . COPY immich_ml immich_ml ARG BUILD_ID diff --git a/machine-learning/immich_ml/models/clip/textual.py b/machine-learning/immich_ml/models/clip/textual.py index 603cd29400..c1b3a9eba4 100644 --- a/machine-learning/immich_ml/models/clip/textual.py +++ b/machine-learning/immich_ml/models/clip/textual.py @@ -10,6 +10,7 @@ from tokenizers import Encoding, Tokenizer from immich_ml.config import log from immich_ml.models.base import InferenceModel +from immich_ml.models.constants import WEBLATE_TO_FLORES200 from immich_ml.models.transforms import clean_text, serialize_np_array from immich_ml.schemas import ModelSession, ModelTask, ModelType @@ -18,8 +19,9 @@ class BaseCLIPTextualEncoder(InferenceModel): depends = [] identity = (ModelType.TEXTUAL, ModelTask.SEARCH) - def _predict(self, inputs: str, **kwargs: Any) -> str: - res: NDArray[np.float32] = self.session.run(None, self.tokenize(inputs))[0][0] + def _predict(self, inputs: str, language: str | None = None, **kwargs: Any) -> str: + tokens = self.tokenize(inputs, language=language) + res: NDArray[np.float32] = self.session.run(None, tokens)[0][0] return serialize_np_array(res) def _load(self) -> ModelSession: @@ -28,6 +30,7 @@ class BaseCLIPTextualEncoder(InferenceModel): self.tokenizer = self._load_tokenizer() tokenizer_kwargs: dict[str, Any] | None = self.text_cfg.get("tokenizer_kwargs") self.canonicalize = tokenizer_kwargs is not None and tokenizer_kwargs.get("clean") == "canonicalize" + self.is_nllb = self.model_name.startswith("nllb") log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") return session @@ -37,7 +40,7 @@ class BaseCLIPTextualEncoder(InferenceModel): pass @abstractmethod - def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + def tokenize(self, text: str, language: str | None = None) -> dict[str, NDArray[np.int32]]: pass @property @@ -92,14 +95,23 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder): return tokenizer - def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + def tokenize(self, text: str, language: str | None = None) -> dict[str, NDArray[np.int32]]: text = clean_text(text, canonicalize=self.canonicalize) + if self.is_nllb and language is not None: + flores_code = WEBLATE_TO_FLORES200.get(language) + if flores_code is None: + no_country = language.split("-")[0] + flores_code = WEBLATE_TO_FLORES200.get(no_country) + if flores_code is None: + log.warning(f"Language '{language}' not found, defaulting to 'en'") + flores_code = "eng_Latn" + text = f"{flores_code}{text}" tokens: Encoding = self.tokenizer.encode(text) return {"text": np.array([tokens.ids], dtype=np.int32)} class MClipTextualEncoder(OpenClipTextualEncoder): - def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + def tokenize(self, text: str, language: str | None = None) -> dict[str, NDArray[np.int32]]: text = clean_text(text, canonicalize=self.canonicalize) tokens: Encoding = self.tokenizer.encode(text) return { diff --git a/machine-learning/immich_ml/models/constants.py b/machine-learning/immich_ml/models/constants.py index 85b5b53991..41b0990f71 100644 --- a/machine-learning/immich_ml/models/constants.py +++ b/machine-learning/immich_ml/models/constants.py @@ -86,6 +86,66 @@ RKNN_SUPPORTED_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"] RKNN_COREMASK_SUPPORTED_SOCS = ["rk3576", "rk3588"] +WEBLATE_TO_FLORES200 = { + "af": "afr_Latn", + "ar": "arb_Arab", + "az": "azj_Latn", + "be": "bel_Cyrl", + "bg": "bul_Cyrl", + "ca": "cat_Latn", + "cs": "ces_Latn", + "da": "dan_Latn", + "de": "deu_Latn", + "el": "ell_Grek", + "en": "eng_Latn", + "es": "spa_Latn", + "et": "est_Latn", + "fa": "pes_Arab", + "fi": "fin_Latn", + "fr": "fra_Latn", + "he": "heb_Hebr", + "hi": "hin_Deva", + "hr": "hrv_Latn", + "hu": "hun_Latn", + "hy": "hye_Armn", + "id": "ind_Latn", + "it": "ita_Latn", + "ja": "jpn_Hira", + "kmr": "kmr_Latn", + "ko": "kor_Hang", + "lb": "ltz_Latn", + "lt": "lit_Latn", + "lv": "lav_Latn", + "mfa": "zsm_Latn", + "mk": "mkd_Cyrl", + "mn": "khk_Cyrl", + "mr": "mar_Deva", + "ms": "zsm_Latn", + "nb-NO": "nob_Latn", + "nn": "nno_Latn", + "nl": "nld_Latn", + "pl": "pol_Latn", + "pt-BR": "por_Latn", + "pt": "por_Latn", + "ro": "ron_Latn", + "ru": "rus_Cyrl", + "sk": "slk_Latn", + "sl": "slv_Latn", + "sr-Cyrl": "srp_Cyrl", + "sv": "swe_Latn", + "ta": "tam_Taml", + "te": "tel_Telu", + "th": "tha_Thai", + "tr": "tur_Latn", + "uk": "ukr_Cyrl", + "ur": "urd_Arab", + "vi": "vie_Latn", + "zh-CN": "zho_Hans", + "zh-Hans": "zho_Hans", + "zh-TW": "zho_Hant", +} + + def get_model_source(model_name: str) -> ModelSource | None: cleaned_name = clean_name(model_name) diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index 4a3696f320..a19ec65c5f 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -494,6 +494,88 @@ class TestCLIP: assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) mock_tokenizer.encode.assert_called_once_with("test search query") + def test_openclip_tokenizer_adds_flores_token_for_nllb( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("immich_ml.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("nllb-clip-base-siglip__mrl", cache_dir="test_cache") + clip_encoder._load() + clip_encoder.tokenize("test search query", language="de") + + mock_tokenizer.encode.assert_called_once_with("deu_Latntest search query") + + def test_openclip_tokenizer_removes_country_code_from_language_for_nllb_if_not_found( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("immich_ml.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("nllb-clip-base-siglip__mrl", cache_dir="test_cache") + clip_encoder._load() + clip_encoder.tokenize("test search query", language="de-CH") + + mock_tokenizer.encode.assert_called_once_with("deu_Latntest search query") + + def test_openclip_tokenizer_falls_back_to_english_for_nllb_if_language_code_not_found( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + warning: mock.Mock, + ) -> None: + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("immich_ml.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("nllb-clip-base-siglip__mrl", cache_dir="test_cache") + clip_encoder._load() + clip_encoder.tokenize("test search query", language="unknown") + + mock_tokenizer.encode.assert_called_once_with("eng_Latntest search query") + warning.assert_called_once_with("Language 'unknown' not found, defaulting to 'en'") + + def test_openclip_tokenizer_does_not_add_flores_token_for_non_nllb_model( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("immich_ml.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache") + clip_encoder._load() + clip_encoder.tokenize("test search query", language="de") + + mock_tokenizer.encode.assert_called_once_with("test search query") + def test_mclip_tokenizer( self, mocker: MockerFixture, diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 7f06e1ba69..0894c74ecf 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -508,16 +508,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.11" +version = "0.115.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/28/c5d26e5860df807241909a961a37d45e10533acef95fc368066c7dd186cd/fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f", size = 294441 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/5d/4d8bbb94f0dbc22732350c06965e40740f4a92ca560e90bb566f4f73af41/fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64", size = 94926 }, + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, ] [[package]] @@ -1583,62 +1583,64 @@ wheels = [ [[package]] name = "orjson" -version = "3.10.15" +version = "3.10.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 }, - { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 }, - { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 }, - { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 }, - { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 }, - { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 }, - { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 }, - { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 }, - { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 }, - { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 }, - { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 }, - { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 }, - { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 }, - { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 }, - { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 }, - { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 }, - { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 }, - { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 }, - { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 }, - { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 }, - { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 }, - { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 }, - { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 }, - { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 }, - { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 }, - { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 }, - { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 }, - { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 }, - { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 }, - { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 }, - { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 }, - { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 }, - { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 }, - { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 }, - { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 }, - { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 }, - { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 }, - { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, - { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, - { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, - { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, - { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, - { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, - { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, - { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, - { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, - { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, - { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, - { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, - { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, + { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 }, + { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 }, + { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 }, + { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 }, + { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 }, + { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 }, + { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 }, + { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 }, + { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 }, + { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 }, + { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 }, + { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 }, + { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 }, + { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 }, + { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 }, + { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 }, + { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 }, + { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 }, + { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 }, + { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 }, + { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 }, + { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 }, + { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 }, + { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 }, + { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 }, + { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 }, + { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 }, + { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 }, + { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 }, + { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 }, + { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 }, + { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 }, + { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 }, + { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 }, + { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 }, + { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 }, + { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 }, + { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 }, + { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 }, + { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 }, + { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 }, + { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 }, + { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 }, + { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 }, + { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 }, + { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 }, + { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 }, + { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 }, + { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 }, + { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 }, ] [[package]] @@ -1787,91 +1789,104 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, + { url = "https://files.pythonhosted.org/packages/29/43/0649ad07e66b36a3fb21442b425bd0348ac162c5e686b36471f363201535/pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e", size = 2042968 }, + { url = "https://files.pythonhosted.org/packages/a0/a6/975fea4774a459e495cb4be288efd8b041ac756a0a763f0b976d0861334b/pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518", size = 1860347 }, + { url = "https://files.pythonhosted.org/packages/aa/49/7858dadad305101a077ec4d0c606b6425a2b134ea8d858458a6d287fd871/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73", size = 1910060 }, + { url = "https://files.pythonhosted.org/packages/8d/4f/6522527911d9c5fe6d76b084d8b388d5c84b09d113247b39f91937500b34/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207", size = 1997129 }, + { url = "https://files.pythonhosted.org/packages/75/d0/06f396da053e3d73001ea4787e56b4d7132a87c0b5e2e15a041e808c35cd/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b", size = 2140389 }, + { url = "https://files.pythonhosted.org/packages/f5/6b/b9ff5b69cd4ef007cf665463f3be2e481dc7eb26c4a55b2f57a94308c31a/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f", size = 2754237 }, + { url = "https://files.pythonhosted.org/packages/53/80/b4879de375cdf3718d05fcb60c9aa1f119d28e261dafa51b6a69c78f7178/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5", size = 2007433 }, + { url = "https://files.pythonhosted.org/packages/46/24/54054713dc0af98a94eab37e0f4294dfd5cd8f70b2ca9dcdccd15709fd7e/pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276", size = 2123980 }, + { url = "https://files.pythonhosted.org/packages/3a/4c/257c1cb89e14cfa6e95ebcb91b308eb1dd2b348340ff76a6e6fcfa9969e1/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760", size = 2087433 }, + { url = "https://files.pythonhosted.org/packages/0c/62/927df8a39ad78ef7b82c5446e01dec9bb0043e1ad71d8f426062f5f014db/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa", size = 2260242 }, + { url = "https://files.pythonhosted.org/packages/74/f2/389414f7c77a100954e84d6f52a82bd1788ae69db72364376d8a73b38765/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c", size = 2258227 }, + { url = "https://files.pythonhosted.org/packages/53/99/94516313e15d906a1264bb40faf24a01a4af4e2ca8a7c10dd173b6513c5a/pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025", size = 1925523 }, + { url = "https://files.pythonhosted.org/packages/7d/67/cc789611c6035a0b71305a1ec6ba196256ced76eba8375f316f840a70456/pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc", size = 1951872 }, + { url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 }, + { url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 }, + { url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 }, + { url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 }, + { url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 }, + { url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 }, + { url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 }, + { url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 }, + { url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 }, + { url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 }, + { url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 }, + { url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 }, + { url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 }, + { url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 }, + { url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 }, + { url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 }, + { url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 }, + { url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 }, + { url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 }, + { url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 }, + { url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 }, + { url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 }, + { url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 }, + { url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 }, + { url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 }, + { url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 }, + { url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 }, + { url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 }, + { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 }, + { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 }, + { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 }, + { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 }, + { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 }, + { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 }, + { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 }, + { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 }, + { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 }, + { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 }, + { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 }, + { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 }, + { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 }, + { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 }, + { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 }, + { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 }, + { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, + { url = "https://files.pythonhosted.org/packages/44/77/85e173b715e1a277ce934f28d877d82492df13e564fa68a01c96f36a47ad/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50", size = 2040129 }, + { url = "https://files.pythonhosted.org/packages/33/e7/33da5f8a94bbe2191cfcd15bd6d16ecd113e67da1b8c78d3cc3478112dab/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c", size = 1872656 }, + { url = "https://files.pythonhosted.org/packages/b4/7a/9600f222bea840e5b9ba1f17c0acc79b669b24542a78c42c6a10712c0aae/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1", size = 1903731 }, + { url = "https://files.pythonhosted.org/packages/81/d2/94c7ca4e24c5dcfb74df92e0836c189e9eb6814cf62d2f26a75ea0a906db/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b", size = 2083966 }, + { url = "https://files.pythonhosted.org/packages/b8/74/a0259989d220e8865ed6866a6d40539e40fa8f507e587e35d2414cc081f8/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d", size = 2118951 }, + { url = "https://files.pythonhosted.org/packages/13/4c/87405ed04d6d07597920b657f082a8e8e58bf3034178bb9044b4d57a91e2/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442", size = 2079632 }, + { url = "https://files.pythonhosted.org/packages/5a/4c/bcb02970ef91d4cd6de7c6893101302637da456bc8b52c18ea0d047b55ce/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a", size = 2250541 }, + { url = "https://files.pythonhosted.org/packages/a3/2b/dbe5450c4cd904be5da736dcc7f2357b828199e29e38de19fc81f988b288/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330", size = 2255685 }, + { url = "https://files.pythonhosted.org/packages/ca/a6/ca1d35f695d81f639c5617fc9efb44caad21a9463383fa45364b3044175a/pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae", size = 2082395 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 }, + { url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 }, + { url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 }, + { url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 }, + { url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 }, + { url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 }, + { url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 }, + { url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 }, ] [[package]] @@ -1933,14 +1948,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.3" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, ] [[package]] @@ -2444,23 +2459,23 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20241230" +version = "6.0.12.20250402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 }, + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, ] [[package]] name = "types-requests" -version = "2.32.0.20250306" +version = "2.32.0.20250328" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/1a/beaeff79ef9efd186566ba5f0d95b44ae21f6d31e9413bcfbef3489b6ae3/types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", size = 23012 } +sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/26/645d89f56004aa0ba3b96fec27793e3c7e62b40982ee069e52568922b6db/types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b", size = 20673 }, + { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 }, ] [[package]] @@ -2477,20 +2492,20 @@ wheels = [ [[package]] name = "types-simplejson" -version = "3.20.0.20250318" +version = "3.20.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/bb/c19c0863ce03b950a2e6319e668a3e3f51c977368d6a1da529893e794162/types_simplejson-3.20.0.20250318.tar.gz", hash = "sha256:5ada2caa2f76826a90b97985f7b0caf55088a23ed4eb9c39fc1f5cb00b985e09", size = 9966 } +sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/5a/adb04301014b18324ec88d5fcfe0ffc262ffc68c443cfe79e32b6cc68a35/types_simplejson-3.20.0.20250318-py3-none-any.whl", hash = "sha256:e8c9cdb06b566b6ca1c7bf3d4c97fbd69a6f6a5aea760ef127f31e638a094c26", size = 10336 }, + { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462 }, ] [[package]] name = "types-ujson" -version = "5.10.0.20240515" +version = "5.10.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/49/abb4bcb9f2258f785edbf236b517c3e7ba8a503a8cbce6b5895930586cc0/types-ujson-5.10.0.20240515.tar.gz", hash = "sha256:ceae7127f0dafe4af5dd0ecf98ee13e9d75951ef963b5c5a9b7ea92e0d71f0d7", size = 3571 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/1f/9d018cee3d09ab44a5211f0b5ed9b0422ad9a8c226bf3967f5884498d8f0/types_ujson-5.10.0.20240515-py3-none-any.whl", hash = "sha256:02bafc36b3a93d2511757a64ff88bd505e0a57fba08183a9150fbcfcb2015310", size = 2757 }, + { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644 }, ] [[package]] @@ -2502,6 +2517,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, +] + [[package]] name = "urllib3" version = "2.1.0" diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 085449756d..07c6f65b71 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -36,6 +36,8 @@ analyzer: exclude: - openapi/** - lib/generated_plugin_registrant.dart + - lib/**/*.g.dart + - lib/**/*.drift.dart plugins: - custom_lint diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index ba167dbe20..612e5084d2 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" => 191, - "android.injected.version.name" => "1.130.3", + "android.injected.version.code" => 193, + "android.injected.version.name" => "1.131.3", } ) 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/build.yaml b/mobile/build.yaml new file mode 100644 index 0000000000..d5de77a377 --- /dev/null +++ b/mobile/build.yaml @@ -0,0 +1,24 @@ +targets: + $default: + builders: + #drift @DriftDatabase() + drift_dev: + # Disable default builder to use modular builder instead + enabled: false + drift_dev:analyzer: + enabled: true + options: &drift_options + store_date_time_values_as_text: true + named_parameters: true + write_from_json_string_constructor: false + data_class_to_companions: false + # Required for make-migrations + databases: + main: lib/infrastructure/repositories/db.repository.dart + generate_for: &drift_generate_for + - lib/infrastructure/entities/*.dart + - lib/infrastructure/repositories/db.repository.dart + drift_dev:modular: + enabled: true + options: *drift_options + generate_for: *drift_generate_for \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json new file mode 100644 index 0000000000..1870ef477f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]} \ No newline at end of file diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 4f030b0495..28ce59feb1 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -201,41 +201,41 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_downloader: 3ca0e156ad83a9fc1c8300f5f7c38e94e2d0bf51 - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + background_downloader: b42a56120f5348bff70e74222f0e9e6f7f1a1537 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 - flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 - flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab - flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603 - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f - geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097 + flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 + flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26 MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e - maplibre_gl: be7b98f1c3ed75bf77f321eec04df359d0ff6f62 - native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c - network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9 + native_video_player: b65c58951ede2f93d103a25366bdebca95081265 + network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 - share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c + share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 88e71e4323..098683be2f 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 = 199; + CURRENT_PROJECT_VERSION = 200; 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 = 199; + CURRENT_PROJECT_VERSION = 200; 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 = 199; + CURRENT_PROJECT_VERSION = 200; 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 = 199; + CURRENT_PROJECT_VERSION = 200; 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 = 199; + CURRENT_PROJECT_VERSION = 200; 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 = 199; + CURRENT_PROJECT_VERSION = 200; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index cd6d6e057a..f83de00158 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.130.3 + 1.131.0 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 199 + 200 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 9a0429126f..4853e9be43 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.130.3" + version_number: "1.131.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index ad241a8c48..abf2e5620b 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,32 +1,4 @@ -import 'dart:ui'; - -enum AvatarColor { - // do not change this order or reuse indices for other purposes, adding is OK - primary, - pink, - red, - yellow, - blue, - green, - purple, - orange, - gray, - amber; - - Color toColor({bool isDarkTheme = false}) => switch (this) { - AvatarColor.primary => - isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), - AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), - AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), - AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), - AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246), - AvatarColor.green => const Color.fromARGB(255, 22, 163, 74), - AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234), - AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12), - AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99), - AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6), - }; -} +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; // TODO: Rename to User once Isar is removed class UserDto { diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart new file mode 100644 index 0000000000..1586384422 --- /dev/null +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -0,0 +1,105 @@ +import 'dart:ui'; + +enum AvatarColor { + // do not change this order or reuse indices for other purposes, adding is OK + primary("primary"), + pink("pink"), + red("red"), + yellow("yellow"), + blue("blue"), + green("green"), + purple("purple"), + orange("orange"), + gray("gray"), + amber("amber"); + + final String value; + const AvatarColor(this.value); + + Color toColor({bool isDarkTheme = false}) => switch (this) { + AvatarColor.primary => + isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF), + AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182), + AvatarColor.red => const Color.fromARGB(255, 239, 68, 68), + AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8), + AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246), + AvatarColor.green => const Color.fromARGB(255, 22, 163, 74), + AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234), + AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12), + AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99), + AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6), + }; +} + +class UserPreferences { + final bool foldersEnabled; + final bool memoriesEnabled; + final bool peopleEnabled; + final bool ratingsEnabled; + final bool sharedLinksEnabled; + final bool tagsEnabled; + final AvatarColor userAvatarColor; + final bool showSupportBadge; + + const UserPreferences({ + this.foldersEnabled = false, + this.memoriesEnabled = true, + this.peopleEnabled = true, + this.ratingsEnabled = false, + this.sharedLinksEnabled = true, + this.tagsEnabled = false, + this.userAvatarColor = AvatarColor.primary, + this.showSupportBadge = true, + }); + + UserPreferences copyWith({ + bool? foldersEnabled, + bool? memoriesEnabled, + bool? peopleEnabled, + bool? ratingsEnabled, + bool? sharedLinksEnabled, + bool? tagsEnabled, + AvatarColor? userAvatarColor, + bool? showSupportBadge, + }) { + return UserPreferences( + foldersEnabled: foldersEnabled ?? this.foldersEnabled, + memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, + peopleEnabled: peopleEnabled ?? this.peopleEnabled, + ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled, + sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled, + tagsEnabled: tagsEnabled ?? this.tagsEnabled, + userAvatarColor: userAvatarColor ?? this.userAvatarColor, + showSupportBadge: showSupportBadge ?? this.showSupportBadge, + ); + } + + Map toMap() { + final preferences = {}; + preferences["folders-Enabled"] = foldersEnabled; + preferences["memories-Enabled"] = memoriesEnabled; + preferences["people-Enabled"] = peopleEnabled; + preferences["ratings-Enabled"] = ratingsEnabled; + preferences["sharedLinks-Enabled"] = sharedLinksEnabled; + preferences["tags-Enabled"] = tagsEnabled; + preferences["avatar-Color"] = userAvatarColor.value; + preferences["purchase-ShowSupportBadge"] = showSupportBadge; + return preferences; + } + + factory UserPreferences.fromMap(Map map) { + return UserPreferences( + foldersEnabled: map["folders-Enabled"] as bool? ?? false, + memoriesEnabled: map["memories-Enabled"] as bool? ?? true, + peopleEnabled: map["people-Enabled"] as bool? ?? true, + ratingsEnabled: map["ratings-Enabled"] as bool? ?? false, + sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true, + tagsEnabled: map["tags-Enabled"] as bool? ?? false, + userAvatarColor: AvatarColor.values.firstWhere( + (e) => e.value == map["avatar-Color"] as String?, + orElse: () => AvatarColor.primary, + ), + showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true, + ); + } +} diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart new file mode 100644 index 0000000000..b7925a8eea --- /dev/null +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class PartnerEntity extends Table with DriftDefaultsMixin { + const PartnerEntity(); + + BlobColumn get sharedById => + blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + BlobColumn get sharedWithId => + blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); + + @override + Set get primaryKey => {sharedById, sharedWithId}; +} diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart new file mode 100644 index 0000000000..974a9e3c30 --- /dev/null +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -0,0 +1,610 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' + as i1; +import 'dart:typed_data' as i2; +import 'package:immich_mobile/infrastructure/entities/partner.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion + Function({ + required i2.Uint8List sharedById, + required i2.Uint8List sharedWithId, + i0.Value inTimeline, +}); +typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion + Function({ + i0.Value sharedById, + i0.Value sharedWithId, + i0.Value inTimeline, +}); + +final class $$PartnerEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$PartnerEntityTable, i1.PartnerEntityData> { + $$PartnerEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('partner_entity') + .sharedById, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get sharedById { + final $_column = $_itemColumn('shared_by_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('partner_entity') + .sharedWithId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get sharedWithId { + final $_column = $_itemColumn('shared_with_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$PartnerEntityTableFilterComposer + extends i0.Composer { + $$PartnerEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get inTimeline => $composableBuilder( + column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column)); + + i5.$$UserEntityTableFilterComposer get sharedById { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedById, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$UserEntityTableFilterComposer get sharedWithId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedWithId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PartnerEntityTableOrderingComposer + extends i0.Composer { + $$PartnerEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get inTimeline => $composableBuilder( + column: $table.inTimeline, + builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get sharedById { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedById, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$UserEntityTableOrderingComposer get sharedWithId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedWithId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PartnerEntityTableAnnotationComposer + extends i0.Composer { + $$PartnerEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get inTimeline => $composableBuilder( + column: $table.inTimeline, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get sharedById { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedById, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$UserEntityTableAnnotationComposer get sharedWithId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.sharedWithId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$PartnerEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$PartnerEntityTable, + i1.PartnerEntityData, + i1.$$PartnerEntityTableFilterComposer, + i1.$$PartnerEntityTableOrderingComposer, + i1.$$PartnerEntityTableAnnotationComposer, + $$PartnerEntityTableCreateCompanionBuilder, + $$PartnerEntityTableUpdateCompanionBuilder, + (i1.PartnerEntityData, i1.$$PartnerEntityTableReferences), + i1.PartnerEntityData, + i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})> { + $$PartnerEntityTableTableManager( + i0.GeneratedDatabase db, i1.$PartnerEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$PartnerEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$PartnerEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value sharedById = const i0.Value.absent(), + i0.Value sharedWithId = const i0.Value.absent(), + i0.Value inTimeline = const i0.Value.absent(), + }) => + i1.PartnerEntityCompanion( + sharedById: sharedById, + sharedWithId: sharedWithId, + inTimeline: inTimeline, + ), + createCompanionCallback: ({ + required i2.Uint8List sharedById, + required i2.Uint8List sharedWithId, + i0.Value inTimeline = const i0.Value.absent(), + }) => + i1.PartnerEntityCompanion.insert( + sharedById: sharedById, + sharedWithId: sharedWithId, + inTimeline: inTimeline, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$PartnerEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({sharedById = false, sharedWithId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (sharedById) { + state = state.withJoin( + currentTable: table, + currentColumn: table.sharedById, + referencedTable: + i1.$$PartnerEntityTableReferences._sharedByIdTable(db), + referencedColumn: i1.$$PartnerEntityTableReferences + ._sharedByIdTable(db) + .id, + ) as T; + } + if (sharedWithId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.sharedWithId, + referencedTable: i1.$$PartnerEntityTableReferences + ._sharedWithIdTable(db), + referencedColumn: i1.$$PartnerEntityTableReferences + ._sharedWithIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$PartnerEntityTable, + i1.PartnerEntityData, + i1.$$PartnerEntityTableFilterComposer, + i1.$$PartnerEntityTableOrderingComposer, + i1.$$PartnerEntityTableAnnotationComposer, + $$PartnerEntityTableCreateCompanionBuilder, + $$PartnerEntityTableUpdateCompanionBuilder, + (i1.PartnerEntityData, i1.$$PartnerEntityTableReferences), + i1.PartnerEntityData, + i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>; + +class $PartnerEntityTable extends i3.PartnerEntity + with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $PartnerEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _sharedByIdMeta = + const i0.VerificationMeta('sharedById'); + @override + late final i0.GeneratedColumn sharedById = + i0.GeneratedColumn('shared_by_id', aliasedName, false, + type: i0.DriftSqlType.blob, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _sharedWithIdMeta = + const i0.VerificationMeta('sharedWithId'); + @override + late final i0.GeneratedColumn sharedWithId = + i0.GeneratedColumn('shared_with_id', aliasedName, false, + type: i0.DriftSqlType.blob, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _inTimelineMeta = + const i0.VerificationMeta('inTimeline'); + @override + late final i0.GeneratedColumn inTimeline = i0.GeneratedColumn( + 'in_timeline', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + @override + List get $columns => + [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('shared_by_id')) { + context.handle( + _sharedByIdMeta, + sharedById.isAcceptableOrUnknown( + data['shared_by_id']!, _sharedByIdMeta)); + } else if (isInserting) { + context.missing(_sharedByIdMeta); + } + if (data.containsKey('shared_with_id')) { + context.handle( + _sharedWithIdMeta, + sharedWithId.isAcceptableOrUnknown( + data['shared_with_id']!, _sharedWithIdMeta)); + } else if (isInserting) { + context.missing(_sharedWithIdMeta); + } + if (data.containsKey('in_timeline')) { + context.handle( + _inTimelineMeta, + inTimeline.isAcceptableOrUnknown( + data['in_timeline']!, _inTimelineMeta)); + } + return context; + } + + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + i1.PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.PartnerEntityData( + sharedById: attachedDatabase.typeMapping + .read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + $PartnerEntityTable createAlias(String alias) { + return $PartnerEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends i0.DataClass + implements i0.Insertable { + final i2.Uint8List sharedById; + final i2.Uint8List sharedWithId; + final bool inTimeline; + const PartnerEntityData( + {required this.sharedById, + required this.sharedWithId, + required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = i0.Variable(sharedById); + map['shared_with_id'] = i0.Variable(sharedWithId); + map['in_timeline'] = i0.Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + i1.PartnerEntityData copyWith( + {i2.Uint8List? sharedById, + i2.Uint8List? sharedWithId, + bool? inTimeline}) => + i1.PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(i1.PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: + data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: + data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById), + i0.$driftBlobEquality.hash(sharedWithId), inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.PartnerEntityData && + i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) && + i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends i0.UpdateCompanion { + final i0.Value sharedById; + final i0.Value sharedWithId; + final i0.Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const i0.Value.absent(), + this.sharedWithId = const i0.Value.absent(), + this.inTimeline = const i0.Value.absent(), + }); + PartnerEntityCompanion.insert({ + required i2.Uint8List sharedById, + required i2.Uint8List sharedWithId, + this.inTimeline = const i0.Value.absent(), + }) : sharedById = i0.Value(sharedById), + sharedWithId = i0.Value(sharedWithId); + static i0.Insertable custom({ + i0.Expression? sharedById, + i0.Expression? sharedWithId, + i0.Expression? inTimeline, + }) { + return i0.RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + i1.PartnerEntityCompanion copyWith( + {i0.Value? sharedById, + i0.Value? sharedWithId, + i0.Value? inTimeline}) { + return i1.PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = i0.Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = i0.Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = i0.Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 710856d9f7..955b2267d1 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,4 +1,7 @@ +import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; @@ -71,3 +74,20 @@ class User { quotaSizeInBytes: quotaSizeInBytes, ); } + +class UserEntity extends Table with DriftDefaultsMixin { + const UserEntity(); + + BlobColumn get id => blob()(); + TextColumn get name => text()(); + BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); + TextColumn get email => text()(); + TextColumn get profileImagePath => text().nullable()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + // Quota + IntColumn get quotaSizeInBytes => integer().nullable()(); + IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart new file mode 100644 index 0000000000..474746a792 --- /dev/null +++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart @@ -0,0 +1,656 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i1; +import 'dart:typed_data' as i2; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion + Function({ + required i2.Uint8List id, + required String name, + i0.Value isAdmin, + required String email, + i0.Value profileImagePath, + i0.Value updatedAt, + i0.Value quotaSizeInBytes, + i0.Value quotaUsageInBytes, +}); +typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion + Function({ + i0.Value id, + i0.Value name, + i0.Value isAdmin, + i0.Value email, + i0.Value profileImagePath, + i0.Value updatedAt, + i0.Value quotaSizeInBytes, + i0.Value quotaUsageInBytes, +}); + +class $$UserEntityTableFilterComposer + extends i0.Composer { + $$UserEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isAdmin => $composableBuilder( + column: $table.isAdmin, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get email => $composableBuilder( + column: $table.email, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get profileImagePath => $composableBuilder( + column: $table.profileImagePath, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, + builder: (column) => i0.ColumnFilters(column)); +} + +class $$UserEntityTableOrderingComposer + extends i0.Composer { + $$UserEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isAdmin => $composableBuilder( + column: $table.isAdmin, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get email => $composableBuilder( + column: $table.email, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get profileImagePath => $composableBuilder( + column: $table.profileImagePath, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, + builder: (column) => i0.ColumnOrderings(column)); +} + +class $$UserEntityTableAnnotationComposer + extends i0.Composer { + $$UserEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get isAdmin => + $composableBuilder(column: $table.isAdmin, builder: (column) => column); + + i0.GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + i0.GeneratedColumn get profileImagePath => $composableBuilder( + column: $table.profileImagePath, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get quotaSizeInBytes => $composableBuilder( + column: $table.quotaSizeInBytes, builder: (column) => column); + + i0.GeneratedColumn get quotaUsageInBytes => $composableBuilder( + column: $table.quotaUsageInBytes, builder: (column) => column); +} + +class $$UserEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$UserEntityTable, + i1.UserEntityData, + i1.$$UserEntityTableFilterComposer, + i1.$$UserEntityTableOrderingComposer, + i1.$$UserEntityTableAnnotationComposer, + $$UserEntityTableCreateCompanionBuilder, + $$UserEntityTableUpdateCompanionBuilder, + ( + i1.UserEntityData, + i0.BaseReferences + ), + i1.UserEntityData, + i0.PrefetchHooks Function()> { + $$UserEntityTableTableManager( + i0.GeneratedDatabase db, i1.$UserEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$UserEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$UserEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$UserEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value isAdmin = const i0.Value.absent(), + i0.Value email = const i0.Value.absent(), + i0.Value profileImagePath = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value quotaSizeInBytes = const i0.Value.absent(), + i0.Value quotaUsageInBytes = const i0.Value.absent(), + }) => + i1.UserEntityCompanion( + id: id, + name: name, + isAdmin: isAdmin, + email: email, + profileImagePath: profileImagePath, + updatedAt: updatedAt, + quotaSizeInBytes: quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes, + ), + createCompanionCallback: ({ + required i2.Uint8List id, + required String name, + i0.Value isAdmin = const i0.Value.absent(), + required String email, + i0.Value profileImagePath = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value quotaSizeInBytes = const i0.Value.absent(), + i0.Value quotaUsageInBytes = const i0.Value.absent(), + }) => + i1.UserEntityCompanion.insert( + id: id, + name: name, + isAdmin: isAdmin, + email: email, + profileImagePath: profileImagePath, + updatedAt: updatedAt, + quotaSizeInBytes: quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$UserEntityTable, + i1.UserEntityData, + i1.$$UserEntityTableFilterComposer, + i1.$$UserEntityTableOrderingComposer, + i1.$$UserEntityTableAnnotationComposer, + $$UserEntityTableCreateCompanionBuilder, + $$UserEntityTableUpdateCompanionBuilder, + ( + i1.UserEntityData, + i0.BaseReferences + ), + i1.UserEntityData, + i0.PrefetchHooks Function()>; + +class $UserEntityTable extends i3.UserEntity + with i0.TableInfo<$UserEntityTable, i1.UserEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $UserEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = + i0.GeneratedColumn('id', aliasedName, false, + type: i0.DriftSqlType.blob, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isAdminMeta = + const i0.VerificationMeta('isAdmin'); + @override + late final i0.GeneratedColumn isAdmin = i0.GeneratedColumn( + 'is_admin', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _emailMeta = + const i0.VerificationMeta('email'); + @override + late final i0.GeneratedColumn email = i0.GeneratedColumn( + 'email', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _profileImagePathMeta = + const i0.VerificationMeta('profileImagePath'); + @override + late final i0.GeneratedColumn profileImagePath = + i0.GeneratedColumn('profile_image_path', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _quotaSizeInBytesMeta = + const i0.VerificationMeta('quotaSizeInBytes'); + @override + late final i0.GeneratedColumn quotaSizeInBytes = i0.GeneratedColumn( + 'quota_size_in_bytes', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _quotaUsageInBytesMeta = + const i0.VerificationMeta('quotaUsageInBytes'); + @override + late final i0.GeneratedColumn quotaUsageInBytes = + i0.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0)); + @override + List get $columns => [ + id, + name, + isAdmin, + email, + profileImagePath, + updatedAt, + quotaSizeInBytes, + quotaUsageInBytes + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('is_admin')) { + context.handle(_isAdminMeta, + isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta)); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, email.isAcceptableOrUnknown(data['email']!, _emailMeta)); + } else if (isInserting) { + context.missing(_emailMeta); + } + if (data.containsKey('profile_image_path')) { + context.handle( + _profileImagePathMeta, + profileImagePath.isAcceptableOrUnknown( + data['profile_image_path']!, _profileImagePathMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('quota_size_in_bytes')) { + context.handle( + _quotaSizeInBytesMeta, + quotaSizeInBytes.isAcceptableOrUnknown( + data['quota_size_in_bytes']!, _quotaSizeInBytesMeta)); + } + if (data.containsKey('quota_usage_in_bytes')) { + context.handle( + _quotaUsageInBytesMeta, + quotaUsageInBytes.isAcceptableOrUnknown( + data['quota_usage_in_bytes']!, _quotaUsageInBytesMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.UserEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + $UserEntityTable createAlias(String alias) { + return $UserEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends i0.DataClass + implements i0.Insertable { + final i2.Uint8List id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['is_admin'] = i0.Variable(isAdmin); + map['email'] = i0.Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = i0.Variable(profileImagePath); + } + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = i0.Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = i0.Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + i1.UserEntityData copyWith( + {i2.Uint8List? id, + String? name, + bool? isAdmin, + String? email, + i0.Value profileImagePath = const i0.Value.absent(), + DateTime? updatedAt, + i0.Value quotaSizeInBytes = const i0.Value.absent(), + int? quotaUsageInBytes}) => + i1.UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present + ? profileImagePath.value + : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present + ? quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(i1.UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present + ? data.profileImagePath.value + : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin, + email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.UserEntityData && + i0.$driftBlobEquality.equals(other.id, this.id) && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value isAdmin; + final i0.Value email; + final i0.Value profileImagePath; + final i0.Value updatedAt; + final i0.Value quotaSizeInBytes; + final i0.Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.isAdmin = const i0.Value.absent(), + this.email = const i0.Value.absent(), + this.profileImagePath = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.quotaSizeInBytes = const i0.Value.absent(), + this.quotaUsageInBytes = const i0.Value.absent(), + }); + UserEntityCompanion.insert({ + required i2.Uint8List id, + required String name, + this.isAdmin = const i0.Value.absent(), + required String email, + this.profileImagePath = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.quotaSizeInBytes = const i0.Value.absent(), + this.quotaUsageInBytes = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + email = i0.Value(email); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? isAdmin, + i0.Expression? email, + i0.Expression? profileImagePath, + i0.Expression? updatedAt, + i0.Expression? quotaSizeInBytes, + i0.Expression? quotaUsageInBytes, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + i1.UserEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? isAdmin, + i0.Value? email, + i0.Value? profileImagePath, + i0.Value? updatedAt, + i0.Value? quotaSizeInBytes, + i0.Value? quotaUsageInBytes}) { + return i1.UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = i0.Variable(isAdmin.value); + } + if (email.present) { + map['email'] = i0.Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = i0.Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = i0.Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = i0.Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart new file mode 100644 index 0000000000..ebbfeebadd --- /dev/null +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class UserMetadataEntity extends Table with DriftDefaultsMixin { + const UserMetadataEntity(); + + BlobColumn get userId => + blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get preferences => text().map(userPreferenceConverter)(); + + @override + Set get primaryKey => {userId}; +} + +final JsonTypeConverter2 + userPreferenceConverter = TypeConverter.json2( + fromJson: (json) => UserPreferences.fromMap(json as Map), + toJson: (pref) => pref.toMap(), +); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart new file mode 100644 index 0000000000..9829fd1acc --- /dev/null +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart @@ -0,0 +1,468 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' + as i1; +import 'dart:typed_data' as i2; +import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$UserMetadataEntityTableCreateCompanionBuilder + = i1.UserMetadataEntityCompanion Function({ + required i2.Uint8List userId, + required i3.UserPreferences preferences, +}); +typedef $$UserMetadataEntityTableUpdateCompanionBuilder + = i1.UserMetadataEntityCompanion Function({ + i0.Value userId, + i0.Value preferences, +}); + +final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$UserMetadataEntityTable, + i1.UserMetadataEntityData> { + $$UserMetadataEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet( + 'user_metadata_entity') + .userId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$UserMetadataEntityTableFilterComposer + extends i0.Composer { + $$UserMetadataEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnWithTypeConverterFilters + get preferences => $composableBuilder( + column: $table.preferences, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get userId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$UserMetadataEntityTableOrderingComposer + extends i0.Composer { + $$UserMetadataEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get preferences => $composableBuilder( + column: $table.preferences, + builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get userId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$UserMetadataEntityTableAnnotationComposer + extends i0.Composer { + $$UserMetadataEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumnWithTypeConverter + get preferences => $composableBuilder( + column: $table.preferences, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get userId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$UserMetadataEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$UserMetadataEntityTable, + i1.UserMetadataEntityData, + i1.$$UserMetadataEntityTableFilterComposer, + i1.$$UserMetadataEntityTableOrderingComposer, + i1.$$UserMetadataEntityTableAnnotationComposer, + $$UserMetadataEntityTableCreateCompanionBuilder, + $$UserMetadataEntityTableUpdateCompanionBuilder, + (i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences), + i1.UserMetadataEntityData, + i0.PrefetchHooks Function({bool userId})> { + $$UserMetadataEntityTableTableManager( + i0.GeneratedDatabase db, i1.$UserMetadataEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => i1 + .$$UserMetadataEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$UserMetadataEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$UserMetadataEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value userId = const i0.Value.absent(), + i0.Value preferences = const i0.Value.absent(), + }) => + i1.UserMetadataEntityCompanion( + userId: userId, + preferences: preferences, + ), + createCompanionCallback: ({ + required i2.Uint8List userId, + required i3.UserPreferences preferences, + }) => + i1.UserMetadataEntityCompanion.insert( + userId: userId, + preferences: preferences, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$UserMetadataEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({userId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (userId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: + i1.$$UserMetadataEntityTableReferences._userIdTable(db), + referencedColumn: i1.$$UserMetadataEntityTableReferences + ._userIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$UserMetadataEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$UserMetadataEntityTable, + i1.UserMetadataEntityData, + i1.$$UserMetadataEntityTableFilterComposer, + i1.$$UserMetadataEntityTableOrderingComposer, + i1.$$UserMetadataEntityTableAnnotationComposer, + $$UserMetadataEntityTableCreateCompanionBuilder, + $$UserMetadataEntityTableUpdateCompanionBuilder, + (i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences), + i1.UserMetadataEntityData, + i0.PrefetchHooks Function({bool userId})>; + +class $UserMetadataEntityTable extends i4.UserMetadataEntity + with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $UserMetadataEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _userIdMeta = + const i0.VerificationMeta('userId'); + @override + late final i0.GeneratedColumn userId = + i0.GeneratedColumn('user_id', aliasedName, false, + type: i0.DriftSqlType.blob, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + @override + late final i0.GeneratedColumnWithTypeConverter + preferences = i0.GeneratedColumn( + 'preferences', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + i1.$UserMetadataEntityTable.$converterpreferences); + @override + List get $columns => [userId, preferences]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {userId}; + @override + i1.UserMetadataEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.UserMetadataEntityData( + userId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!, + preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!), + ); + } + + @override + $UserMetadataEntityTable createAlias(String alias) { + return $UserMetadataEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $converterpreferences = i4.userPreferenceConverter; + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends i0.DataClass + implements i0.Insertable { + final i2.Uint8List userId; + final i3.UserPreferences preferences; + const UserMetadataEntityData( + {required this.userId, required this.preferences}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = i0.Variable(userId); + { + map['preferences'] = i0.Variable( + i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences)); + } + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + preferences: i1.$UserMetadataEntityTable.$converterpreferences + .fromJson(serializer.fromJson(json['preferences'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'preferences': serializer.toJson(i1 + .$UserMetadataEntityTable.$converterpreferences + .toJson(preferences)), + }; + } + + i1.UserMetadataEntityData copyWith( + {i2.Uint8List? userId, i3.UserPreferences? preferences}) => + i1.UserMetadataEntityData( + userId: userId ?? this.userId, + preferences: preferences ?? this.preferences, + ); + UserMetadataEntityData copyWithCompanion( + i1.UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + preferences: + data.preferences.present ? data.preferences.value : this.preferences, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('preferences: $preferences') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(i0.$driftBlobEquality.hash(userId), preferences); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.UserMetadataEntityData && + i0.$driftBlobEquality.equals(other.userId, this.userId) && + other.preferences == this.preferences); +} + +class UserMetadataEntityCompanion + extends i0.UpdateCompanion { + final i0.Value userId; + final i0.Value preferences; + const UserMetadataEntityCompanion({ + this.userId = const i0.Value.absent(), + this.preferences = const i0.Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required i2.Uint8List userId, + required i3.UserPreferences preferences, + }) : userId = i0.Value(userId), + preferences = i0.Value(preferences); + static i0.Insertable custom({ + i0.Expression? userId, + i0.Expression? preferences, + }) { + return i0.RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (preferences != null) 'preferences': preferences, + }); + } + + i1.UserMetadataEntityCompanion copyWith( + {i0.Value? userId, + i0.Value? preferences}) { + return i1.UserMetadataEntityCompanion( + userId: userId ?? this.userId, + preferences: preferences ?? this.preferences, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = i0.Variable(userId.value); + } + if (preferences.present) { + map['preferences'] = i0.Variable(i1 + .$UserMetadataEntityTable.$converterpreferences + .toSql(preferences.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('preferences: $preferences') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 74e182bdee..997714e1b6 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -1,8 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; +import 'db.repository.drift.dart'; + // #zoneTxn is the symbol used by Isar to mark a transaction within the current zone // ref: isar/isar_common.dart const Symbol _kzoneTxn = #zoneTxn; @@ -17,3 +24,35 @@ class IsarDatabaseRepository implements IDatabaseRepository { Future transaction(Future Function() callback) => Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); } + +@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) +class Drift extends $Drift implements IDatabaseRepository { + Drift([QueryExecutor? executor]) + : super( + executor ?? + driftDatabase( + name: 'immich', + native: const DriftNativeOptions(shareAcrossIsolates: true), + ), + ); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration => MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA journal_mode = WAL'); + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); +} + +class DriftDatabaseRepository implements IDatabaseRepository { + final Drift _db; + const DriftDatabaseRepository(this._db); + + @override + Future transaction(Future Function() callback) => + _db.transaction(callback); +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart new file mode 100644 index 0000000000..a4c2b31dcd --- /dev/null +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -0,0 +1,67 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' + as i3; + +abstract class $Drift extends i0.GeneratedDatabase { + $Drift(i0.QueryExecutor e) : super(e); + $DriftManager get managers => $DriftManager(this); + late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this); + late final i2.$UserMetadataEntityTable userMetadataEntity = + i2.$UserMetadataEntityTable(this); + late final i3.$PartnerEntityTable partnerEntity = + i3.$PartnerEntityTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [userEntity, userMetadataEntity, partnerEntity]; + @override + i0.StreamQueryUpdateRules get streamUpdateRules => + const i0.StreamQueryUpdateRules( + [ + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('user_metadata_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), + ], + ), + ], + ); + @override + i0.DriftDatabaseOptions get options => + const i0.DriftDatabaseOptions(storeDateTimeAsText: true); +} + +class $DriftManager { + final $Drift _db; + $DriftManager(this._db); + i1.$$UserEntityTableTableManager get userEntity => + i1.$$UserEntityTableTableManager(_db, _db.userEntity); + i2.$$UserMetadataEntityTableTableManager get userMetadataEntity => + i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); + i3.$$PartnerEntityTableTableManager get partnerEntity => + i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); +} diff --git a/mobile/lib/infrastructure/utils/drift_default.mixin.dart b/mobile/lib/infrastructure/utils/drift_default.mixin.dart new file mode 100644 index 0000000000..1ba4589ed5 --- /dev/null +++ b/mobile/lib/infrastructure/utils/drift_default.mixin.dart @@ -0,0 +1,9 @@ +import 'package:drift/drift.dart'; + +mixin DriftDefaultsMixin on Table { + @override + bool get isStrict => true; + + @override + bool get withoutRowId => true; +} diff --git a/mobile/lib/infrastructure/utils/exif.converter.dart b/mobile/lib/infrastructure/utils/exif.converter.dart index 0f6e2b0295..eb9945f454 100644 --- a/mobile/lib/infrastructure/utils/exif.converter.dart +++ b/mobile/lib/infrastructure/utils/exif.converter.dart @@ -1,6 +1,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:openapi/api.dart'; +// TODO: Move to repository once all classes are refactored abstract final class ExifDtoConverter { static ExifInfo fromDto(ExifResponseDto dto) { return ExifInfo( diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart index fcf7ede51c..eb7b24737e 100644 --- a/mobile/lib/infrastructure/utils/user.converter.dart +++ b/mobile/lib/infrastructure/utils/user.converter.dart @@ -1,6 +1,8 @@ import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:openapi/api.dart'; +// TODO: Move to repository once all classes are refactored abstract final class UserConverter { /// Base user dto used where the complete user object is not required static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto( diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 87e7b24e34..598b71ef4e 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -236,6 +236,7 @@ class SearchFilter { String? context; String? filename; String? description; + String? language; Set people; SearchLocationFilter location; SearchCameraFilter camera; @@ -249,6 +250,7 @@ class SearchFilter { this.context, this.filename, this.description, + this.language, required this.people, required this.location, required this.camera, @@ -279,6 +281,7 @@ class SearchFilter { String? context, String? filename, String? description, + String? language, Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, @@ -290,6 +293,7 @@ class SearchFilter { context: context ?? this.context, filename: filename ?? this.filename, description: description ?? this.description, + language: language ?? this.language, people: people ?? this.people, location: location ?? this.location, camera: camera ?? this.camera, @@ -301,7 +305,7 @@ class SearchFilter { @override String toString() { - return 'SearchFilter(context: $context, filename: $filename, description: $description, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)'; + return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)'; } @override @@ -311,6 +315,7 @@ class SearchFilter { return other.context == context && other.filename == filename && other.description == description && + other.language == language && other.people == people && other.location == location && other.camera == camera && @@ -324,6 +329,7 @@ class SearchFilter { return context.hashCode ^ filename.hashCode ^ description.hashCode ^ + language.hashCode ^ people.hashCode ^ location.hashCode ^ camera.hashCode ^ diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index f51be027f5..ab780eeb75 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -263,10 +263,6 @@ class GalleryViewerPage extends HookConsumerWidget { PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { var newAsset = loadAsset(index); - WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(currentAssetProvider.notifier).set(newAsset); - }); - final stackId = newAsset.stackId; if (stackId != null && currentIndex.value == index) { final stackElements = diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index 23685db274..957a119f66 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -44,6 +44,10 @@ class NativeVideoViewerPage extends HookConsumerWidget { final lastVideoPosition = useRef(-1); final isBuffering = useRef(false); + // Used to track whether the video should play when the app + // is brought back to the foreground + final shouldPlayOnForeground = useRef(true); + // When a video is opened through the timeline, `isCurrent` will immediately be true. // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. // If the swipe is completed, `isCurrent` will be true for video B after a delay. @@ -368,6 +372,20 @@ class NativeVideoViewerPage extends HookConsumerWidget { const [], ); + useOnAppLifecycleStateChange((_, state) async { + if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { + controller.value?.play(); + } else if (state == AppLifecycleState.paused) { + final videoPlaying = await controller.value?.isPlaying(); + if (videoPlaying ?? true) { + shouldPlayOnForeground.value = true; + controller.value?.pause(); + } else { + shouldPlayOnForeground.value = false; + } + } + }); + return Stack( children: [ // This remains under the video to avoid flickering diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 9ff8caff1d..b2bed73c6a 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -48,6 +48,8 @@ class SearchPage extends HookConsumerWidget { isFavorite: false, ), mediaType: prefilter?.mediaType ?? AssetType.other, + language: + "${context.locale.languageCode}-${context.locale.countryCode}", ), ); diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index ac1c768df0..d9e8897e97 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -71,8 +71,13 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { Future> getAllByRemoteId( Iterable ids, { AssetState? state, - }) => - _getAllByRemoteIdImpl(ids, state).findAll(); + }) async { + if (ids.isEmpty) { + return []; + } + + return _getAllByRemoteIdImpl(ids, state).findAll(); + } QueryBuilder _getAllByRemoteIdImpl( Iterable ids, diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 2894f5a7de..92f04e2304 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -389,10 +388,7 @@ class AssetService { } Future getAspectRatio(Asset asset) async { - // platform_manager always returns 0 for orientation on iOS, so only prefer it on Android - if (asset.isLocal && Platform.isAndroid) { - await asset.localAsync; - } else if (asset.isRemote) { + if (asset.isRemote) { asset = await loadExif(asset); } else if (asset.isLocal) { await asset.localAsync; diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 4c6c80abf3..44ace78852 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -60,6 +60,7 @@ class SearchService { response = await _apiService.searchApi.searchSmart( SmartSearchDto( query: filter.context!, + language: filter.language, country: filter.location.country, state: filter.location.state, city: filter.location.city, diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index ea54c69e53..611d149d99 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -95,6 +95,11 @@ class BottomGalleryBar extends ConsumerWidget { totalAssets.value -= 1; } + if (isDeleted) { + ref + .read(currentAssetProvider.notifier) + .set(renderList.loadAsset(assetIndex.value)); + } return isDeleted; } diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index b3727e8323..8866cb01b0 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; diff --git a/mobile/makefile b/mobile/makefile index 43bc59c7d4..0931d6c167 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -14,3 +14,6 @@ create_splash: build_release_android: flutter build appbundle + +migrations: + dart run drift_dev make-migrations diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index dfeda7adfd..0ae07e9efd 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.130.3 +- API version: 1.131.3 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -438,6 +438,7 @@ Class | Method | HTTP request | Description - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) + - [SystemConfigGeneratedFullsizeImageDto](doc//SystemConfigGeneratedFullsizeImageDto.md) - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index eddd63a732..3986362c96 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -245,6 +245,7 @@ part 'model/system_config_backups_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_faces_dto.dart'; +part 'model/system_config_generated_fullsize_image_dto.dart'; part 'model/system_config_generated_image_dto.dart'; part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 783e5e375e..5759217f41 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -546,6 +546,8 @@ class ApiClient { return SystemConfigFFmpegDto.fromJson(value); case 'SystemConfigFacesDto': return SystemConfigFacesDto.fromJson(value); + case 'SystemConfigGeneratedFullsizeImageDto': + return SystemConfigGeneratedFullsizeImageDto.fromJson(value); case 'SystemConfigGeneratedImageDto': return SystemConfigGeneratedImageDto.fromJson(value); case 'SystemConfigImageDto': diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index 2a950db411..aa7e2a6f5c 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -23,11 +23,13 @@ class AssetMediaSize { String toJson() => value; + static const fullsize = AssetMediaSize._(r'fullsize'); static const preview = AssetMediaSize._(r'preview'); static const thumbnail = AssetMediaSize._(r'thumbnail'); /// List of all possible values in this [enum][AssetMediaSize]. static const values = [ + fullsize, preview, thumbnail, ]; @@ -68,6 +70,7 @@ class AssetMediaSizeTypeTransformer { AssetMediaSize? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { + case r'fullsize': return AssetMediaSize.fullsize; case r'preview': return AssetMediaSize.preview; case r'thumbnail': return AssetMediaSize.thumbnail; default: diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart index bfb16c6667..55453ed1e8 100644 --- a/mobile/openapi/lib/model/path_type.dart +++ b/mobile/openapi/lib/model/path_type.dart @@ -24,6 +24,7 @@ class PathType { String toJson() => value; static const original = PathType._(r'original'); + static const fullsize = PathType._(r'fullsize'); static const preview = PathType._(r'preview'); static const thumbnail = PathType._(r'thumbnail'); static const encodedVideo = PathType._(r'encoded_video'); @@ -34,6 +35,7 @@ class PathType { /// List of all possible values in this [enum][PathType]. static const values = [ original, + fullsize, preview, thumbnail, encodedVideo, @@ -79,6 +81,7 @@ class PathTypeTypeTransformer { if (data != null) { switch (data) { case r'original': return PathType.original; + case r'fullsize': return PathType.fullsize; case r'preview': return PathType.preview; case r'thumbnail': return PathType.thumbnail; case r'encoded_video': return PathType.encodedVideo; diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index f377c23f22..47c800ff09 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -25,6 +25,7 @@ class SmartSearchDto { this.isNotInAlbum, this.isOffline, this.isVisible, + this.language, this.lensModel, this.libraryId, this.make, @@ -132,6 +133,14 @@ class SmartSearchDto { /// bool? isVisible; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? language; + String? lensModel; String? libraryId; @@ -271,6 +280,7 @@ class SmartSearchDto { other.isNotInAlbum == isNotInAlbum && other.isOffline == isOffline && other.isVisible == isVisible && + other.language == language && other.lensModel == lensModel && other.libraryId == libraryId && other.make == make && @@ -308,6 +318,7 @@ class SmartSearchDto { (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + (isOffline == null ? 0 : isOffline!.hashCode) + (isVisible == null ? 0 : isVisible!.hashCode) + + (language == null ? 0 : language!.hashCode) + (lensModel == null ? 0 : lensModel!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (make == null ? 0 : make!.hashCode) + @@ -331,7 +342,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -395,6 +406,11 @@ class SmartSearchDto { } else { // json[r'isVisible'] = null; } + if (this.language != null) { + json[r'language'] = this.language; + } else { + // json[r'language'] = null; + } if (this.lensModel != null) { json[r'lensModel'] = this.lensModel; } else { @@ -508,6 +524,7 @@ class SmartSearchDto { isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'), isOffline: mapValueOfType(json, r'isOffline'), isVisible: mapValueOfType(json, r'isVisible'), + language: mapValueOfType(json, r'language'), lensModel: mapValueOfType(json, r'lensModel'), libraryId: mapValueOfType(json, r'libraryId'), make: mapValueOfType(json, r'make'), diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart new file mode 100644 index 0000000000..fbeb704b27 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -0,0 +1,117 @@ +// +// 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 SystemConfigGeneratedFullsizeImageDto { + /// Returns a new [SystemConfigGeneratedFullsizeImageDto] instance. + SystemConfigGeneratedFullsizeImageDto({ + required this.enabled, + required this.format, + required this.quality, + }); + + bool enabled; + + ImageFormat format; + + /// Minimum value: 1 + /// Maximum value: 100 + int quality; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedFullsizeImageDto && + other.enabled == enabled && + other.format == format && + other.quality == quality; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (format.hashCode) + + (quality.hashCode); + + @override + String toString() => 'SystemConfigGeneratedFullsizeImageDto[enabled=$enabled, format=$format, quality=$quality]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'format'] = this.format; + json[r'quality'] = this.quality; + return json; + } + + /// Returns a new [SystemConfigGeneratedFullsizeImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigGeneratedFullsizeImageDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigGeneratedFullsizeImageDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigGeneratedFullsizeImageDto( + enabled: mapValueOfType(json, r'enabled')!, + format: ImageFormat.fromJson(json[r'format'])!, + quality: mapValueOfType(json, r'quality')!, + ); + } + 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 = SystemConfigGeneratedFullsizeImageDto.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 = SystemConfigGeneratedFullsizeImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigGeneratedFullsizeImageDto-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] = SystemConfigGeneratedFullsizeImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'format', + 'quality', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 5309f7745c..783eaa7d46 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -15,6 +15,7 @@ class SystemConfigImageDto { SystemConfigImageDto({ required this.colorspace, required this.extractEmbedded, + required this.fullsize, required this.preview, required this.thumbnail, }); @@ -23,6 +24,8 @@ class SystemConfigImageDto { bool extractEmbedded; + SystemConfigGeneratedFullsizeImageDto fullsize; + SystemConfigGeneratedImageDto preview; SystemConfigGeneratedImageDto thumbnail; @@ -31,6 +34,7 @@ class SystemConfigImageDto { bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && other.extractEmbedded == extractEmbedded && + other.fullsize == fullsize && other.preview == preview && other.thumbnail == thumbnail; @@ -39,16 +43,18 @@ class SystemConfigImageDto { // ignore: unnecessary_parenthesis (colorspace.hashCode) + (extractEmbedded.hashCode) + + (fullsize.hashCode) + (preview.hashCode) + (thumbnail.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, fullsize=$fullsize, preview=$preview, thumbnail=$thumbnail]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; json[r'extractEmbedded'] = this.extractEmbedded; + json[r'fullsize'] = this.fullsize; json[r'preview'] = this.preview; json[r'thumbnail'] = this.thumbnail; return json; @@ -65,6 +71,7 @@ class SystemConfigImageDto { return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, + fullsize: SystemConfigGeneratedFullsizeImageDto.fromJson(json[r'fullsize'])!, preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!, thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!, ); @@ -116,6 +123,7 @@ class SystemConfigImageDto { static const requiredKeys = { 'colorspace', 'extractEmbedded', + 'fullsize', 'preview', 'thumbnail', }; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9c841a870e..e79d9f4084 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -206,6 +206,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -382,6 +390,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.2" + drift: + dependency: "direct main" + description: + name: drift + sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5" + url: "https://pub.dev" + source: hosted + version: "2.26.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588" + url: "https://pub.dev" + source: hosted + version: "2.26.0" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922" + url: "https://pub.dev" + source: hosted + version: "0.2.4" dynamic_color: dependency: "direct main" description: @@ -1288,6 +1320,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" riverpod: dependency: transitive description: @@ -1549,6 +1589,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + url: "https://pub.dev" + source: hosted + version: "2.7.5" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3" + url: "https://pub.dev" + source: hosted + version: "0.5.31" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee" + url: "https://pub.dev" + source: hosted + version: "0.41.0" stack_trace: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0978f59a3c..d4ab110a3e 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.130.3+191 +version: 1.131.3+193 environment: sdk: '>=3.3.0 <4.0.0' @@ -73,6 +73,9 @@ dependencies: isar_flutter_libs: # contains Isar Core version: *isar_version hosted: https://pub.isar-community.dev/ + # DB + drift: ^2.23.1 + drift_flutter: ^0.2.4 dependency_overrides: analyzer: ^6.0.0 @@ -99,6 +102,8 @@ dev_dependencies: immich_mobile_immich_lint: path: './immich_lint' fake_async: ^1.3.1 + # Drift generator + drift_dev: ^2.23.1 flutter: uses-material-design: true diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 1dfec9b4b1..764342520f 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; abstract final class UserStub { const UserStub._(); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5ba08ab80b..ac30e9ae97 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7656,7 +7656,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.130.3", + "version": "1.131.3", "contact": {} }, "tags": [], @@ -8660,6 +8660,7 @@ }, "AssetMediaSize": { "enum": [ + "fullsize", "preview", "thumbnail" ], @@ -10442,6 +10443,7 @@ "PathType": { "enum": [ "original", + "fullsize", "preview", "thumbnail", "encoded_video", @@ -11853,6 +11855,9 @@ "isVisible": { "type": "boolean" }, + "language": { + "type": "string" + }, "lensModel": { "nullable": true, "type": "string" @@ -12612,6 +12617,31 @@ ], "type": "object" }, + "SystemConfigGeneratedFullsizeImageDto": { + "properties": { + "enabled": { + "type": "boolean" + }, + "format": { + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] + }, + "quality": { + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "enabled", + "format", + "quality" + ], + "type": "object" + }, "SystemConfigGeneratedImageDto": { "properties": { "format": { @@ -12650,6 +12680,9 @@ "extractEmbedded": { "type": "boolean" }, + "fullsize": { + "$ref": "#/components/schemas/SystemConfigGeneratedFullsizeImageDto" + }, "preview": { "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" }, @@ -12660,6 +12693,7 @@ "required": [ "colorspace", "extractEmbedded", + "fullsize", "preview", "thumbnail" ], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 6ba63ac795..3d3b5b3780 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.130.3", + "version": "1.131.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.130.3", + "version": "1.131.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.13.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz", + "integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 1c9912ac0f..6a06647419 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.130.3", + "version": "1.131.3", "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.10", + "@types/node": "^22.13.14", "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 252ce9bc69..f82f5bc9a7 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.130.3 + * 1.131.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -924,6 +924,7 @@ export type SmartSearchDto = { isNotInAlbum?: boolean; isOffline?: boolean; isVisible?: boolean; + language?: string; lensModel?: string | null; libraryId?: string | null; make?: string; @@ -1178,6 +1179,11 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigGeneratedFullsizeImageDto = { + enabled: boolean; + format: ImageFormat; + quality: number; +}; export type SystemConfigGeneratedImageDto = { format: ImageFormat; quality: number; @@ -1186,6 +1192,7 @@ export type SystemConfigGeneratedImageDto = { export type SystemConfigImageDto = { colorspace: Colorspace; extractEmbedded: boolean; + fullsize: SystemConfigGeneratedFullsizeImageDto; preview: SystemConfigGeneratedImageDto; thumbnail: SystemConfigGeneratedImageDto; }; @@ -3572,6 +3579,7 @@ export enum AssetJobName { TranscodeVideo = "transcode-video" } export enum AssetMediaSize { + Fullsize = "fullsize", Preview = "preview", Thumbnail = "thumbnail" } @@ -3621,6 +3629,7 @@ export enum PathEntityType { } export enum PathType { Original = "original", + Fullsize = "fullsize", Preview = "preview", Thumbnail = "thumbnail", EncodedVideo = "encoded_video", diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 5fe62b9651..f80dbb4691 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -1,39 +1,29 @@ -import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import tsParser from '@typescript-eslint/parser'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import typescriptEslint from 'typescript-eslint'; 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 [ +export default typescriptEslint.config([ + eslintPluginUnicorn.configs.recommended, + eslintPluginPrettierRecommended, + js.configs.recommended, + typescriptEslint.configs.recommended, { ignores: ['eslint.config.mjs'], }, - ...compat.extends( - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'plugin:unicorn/recommended', - ), { - plugins: { - '@typescript-eslint': typescriptEslint, - }, - languageOptions: { globals: { ...globals.node, }, - parser: tsParser, + parser: typescriptEslint.parser, ecmaVersion: 5, sourceType: 'module', @@ -77,6 +67,14 @@ export default [ ], }, ], + + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], }, }, -]; +]); diff --git a/server/package-lock.json b/server/package-lock.json index 1978d15722..02b0d357c0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,13 @@ { "name": "immich", - "version": "1.130.3", + "version": "1.131.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.130.3", + "version": "1.131.3", + "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -19,7 +20,7 @@ "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", "@opentelemetry/auto-instrumentations-node": "^0.57.0", - "@opentelemetry/context-async-hooks": "^1.24.0", + "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/exporter-prometheus": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0", "@react-email/components": "^0.0.34", @@ -55,6 +56,7 @@ "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", + "react-dom": "^19.0.0", "react-email": "^3.0.4", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -88,7 +90,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", @@ -97,17 +99,16 @@ "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", "kysely-codegen": "^0.18.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", + "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -117,6 +118,7 @@ "testcontainers": "^10.18.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", + "typescript-eslint": "^8.28.0", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", "vite-tsconfig-paths": "^5.0.0", @@ -2869,9 +2871,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.0.7.tgz", - "integrity": "sha512-WQBDs7yM8KhEKK/ukJOqse6/qLrDKbIIYJjpYvUH79Y9Fm/+9l6lQ3buqBf6jzwmzTaVCw9GVu3vhIuKlYVYRA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.0.tgz", + "integrity": "sha512-+GQ+q1ASTBvGi0DYHukWi8NVVVLszedwLLqHdLRnJh8rjokt8YTDb7roImvT/YMmYgPvaWBv/4JYdZH4FueLPQ==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", @@ -3220,12 +3222,12 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", - "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.0.tgz", + "integrity": "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==", "license": "Apache-2.0", "engines": { - "node": ">=14" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" @@ -4452,18 +4454,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.0.tgz", - "integrity": "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", @@ -4505,9 +4495,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, "license": "MIT", "engines": { @@ -5259,15 +5249,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.11.tgz", - "integrity": "sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.15.tgz", + "integrity": "sha512-SqXjJrwydXA2OVVAFv9EdCb2kkhEM2+b4ajereGzFSQuK2FN/SlKPklvFMh9sj1sG0tgXwyLGSMgyn3FUx83DA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.19" + "@swc/types": "^0.1.21" }, "engines": { "node": ">=10" @@ -5277,16 +5267,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.11", - "@swc/core-darwin-x64": "1.11.11", - "@swc/core-linux-arm-gnueabihf": "1.11.11", - "@swc/core-linux-arm64-gnu": "1.11.11", - "@swc/core-linux-arm64-musl": "1.11.11", - "@swc/core-linux-x64-gnu": "1.11.11", - "@swc/core-linux-x64-musl": "1.11.11", - "@swc/core-win32-arm64-msvc": "1.11.11", - "@swc/core-win32-ia32-msvc": "1.11.11", - "@swc/core-win32-x64-msvc": "1.11.11" + "@swc/core-darwin-arm64": "1.11.15", + "@swc/core-darwin-x64": "1.11.15", + "@swc/core-linux-arm-gnueabihf": "1.11.15", + "@swc/core-linux-arm64-gnu": "1.11.15", + "@swc/core-linux-arm64-musl": "1.11.15", + "@swc/core-linux-x64-gnu": "1.11.15", + "@swc/core-linux-x64-musl": "1.11.15", + "@swc/core-win32-arm64-msvc": "1.11.15", + "@swc/core-win32-ia32-msvc": "1.11.15", + "@swc/core-win32-x64-msvc": "1.11.15" }, "peerDependencies": { "@swc/helpers": "*" @@ -5298,9 +5288,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.11.tgz", - "integrity": "sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.15.tgz", + "integrity": "sha512-mMoQy6TrYrvhrpi70eD01uu4WeB+Wy+9To5b95gHcyiAMRyd7afnFHo9OcPynk0Ep01PvReiB6hL2hYfNcDKvw==", "cpu": [ "arm64" ], @@ -5315,9 +5305,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.11.tgz", - "integrity": "sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.15.tgz", + "integrity": "sha512-yBWcP5v3OXq1Nxamqh1+qecty3TFRlxAMNXMBzq/Rv6Fu9eOAU6lTSfozO0BaOoETTzorlR2/3Jn+3amyviQMw==", "cpu": [ "x64" ], @@ -5332,9 +5322,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.11.tgz", - "integrity": "sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.15.tgz", + "integrity": "sha512-OprUQ0AvIiA2FCZqDYcnZ1nZhiCABqJPGgC9KwX8p8tC+t1mYkAeboik23S9gxzwGQImMNYYojGbNGTmLATLrA==", "cpu": [ "arm" ], @@ -5349,9 +5339,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.11.tgz", - "integrity": "sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.15.tgz", + "integrity": "sha512-Uq3FjjKEw1CTtFpz7Mi+CC//4KQODQ8vXFx7J/cBO6nj+/Os9J1huyqa1LljlBTCeDXTpeC7qlqO6swZ0HPaJw==", "cpu": [ "arm64" ], @@ -5366,9 +5356,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.11.tgz", - "integrity": "sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.15.tgz", + "integrity": "sha512-G5orst6QzXyTXgOTnjrkYaLaK3emMXBWkQ7CDFyZNCGo6Fztn0vzYcCmr31Cvqs66BsM0sdGbcrBd5br8g/pJg==", "cpu": [ "arm64" ], @@ -5383,9 +5373,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.11.tgz", - "integrity": "sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.15.tgz", + "integrity": "sha512-T0iR9yUcGyo1yLudL73jKbPS4AYo2iAWWH2I9u7QYiRTXPduwkH0nETNr+nsWBsYdMu+H2g169rCiGhhx6FPHw==", "cpu": [ "x64" ], @@ -5400,9 +5390,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.11.tgz", - "integrity": "sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.15.tgz", + "integrity": "sha512-2d8pHehwsHdQ71PRLeJ/XM69t5LCMzf1KZQDTVJTOSWRbuKGArtD+md5lVzTu458gt+JawdUgFdkdHtF7ke0AA==", "cpu": [ "x64" ], @@ -5417,9 +5407,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.11.tgz", - "integrity": "sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.15.tgz", + "integrity": "sha512-Vz5xg03VdYftMvruvziV1doU7B64rQ8rw72bKf2+yflt1gU7BlLk4DPu2IZlUc0Xk8lrVcEDiheXATbHexKsmw==", "cpu": [ "arm64" ], @@ -5434,9 +5424,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.11.tgz", - "integrity": "sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.15.tgz", + "integrity": "sha512-R9jS92ubQgHQfyNVCMnuQfNPeBgAs3QaWC+DqPbhXtOyWUdSGcImbHMDCxShDj+nn8J7bPeb7L4sZqr6gBkZnQ==", "cpu": [ "ia32" ], @@ -5451,9 +5441,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.11.tgz", - "integrity": "sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.15.tgz", + "integrity": "sha512-UpSX492qVVTJQkRBYw3qC49ae4QRHwuC1cDgA47XBP0l31vjR83r3qEYue1Nn173etzGzbDJnygyLpqv/ieCCA==", "cpu": [ "x64" ], @@ -5483,9 +5473,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5493,23 +5483,23 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "10.21.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.21.0.tgz", - "integrity": "sha512-H90Uezr+BqwMS7TA5SJd9qM18SiiAnegyJ5W40mWGRm2kyNgG1AHyEcFtdlOm4z6crBIou55kz+MJLz86KGWpA==", + "version": "10.23.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.23.0.tgz", + "integrity": "sha512-PKuv7cSWxOxW4aOEuw1XyYb7tS8rcPEmg2ez97WTLLnVZj4JaoPJqFDSEJ2OSj8s3+6HqLC6hXDCMFmYhP63/A==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.21.0" + "testcontainers": "^10.23.0" } }, "node_modules/@testcontainers/redis": { - "version": "10.21.0", - "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-10.21.0.tgz", - "integrity": "sha512-52/NUHFz+N8W5Yv1d+mLjuEfb0llgmJDxWALOAknBPTYUfis2uBZqBpmSd906z93x6YMrz7gtMD36dpZLJJJsg==", + "version": "10.23.0", + "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-10.23.0.tgz", + "integrity": "sha512-rMkEdCsjhAPFuagHfI28q/Uvq6Wj/uN0qJxa6bwvenc6qhbzqYlK8iguj3M/cYs5ItDgxZ9J6HxhZKzkc3U1iQ==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.21.0" + "testcontainers": "^10.23.0" } }, "node_modules/@turf/boolean-point-in-polygon": { @@ -5835,9 +5825,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.13.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz", + "integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -5942,9 +5932,9 @@ } }, "node_modules/@types/sanitize-html": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz", - "integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.15.0.tgz", + "integrity": "sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==", "dev": true, "license": "MIT", "dependencies": { @@ -5952,9 +5942,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true, "license": "MIT" }, @@ -6038,9 +6028,9 @@ } }, "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", "dev": true, "license": "MIT", "dependencies": { @@ -6081,17 +6071,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6111,16 +6101,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "engines": { @@ -6136,14 +6126,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6154,14 +6144,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -6178,9 +6168,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "license": "MIT", "engines": { @@ -6192,14 +6182,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6245,16 +6235,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6269,13 +6259,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6300,9 +6290,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", - "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", + "integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6315,7 +6305,7 @@ "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, @@ -6323,8 +6313,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.9", - "vitest": "3.0.9" + "@vitest/browser": "3.1.1", + "vitest": "3.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6333,14 +6323,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -6349,13 +6339,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6386,9 +6376,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "dev": true, "license": "MIT", "dependencies": { @@ -6399,13 +6389,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.1.1", "pathe": "^2.0.3" }, "funding": { @@ -6413,13 +6403,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6428,9 +6418,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6441,13 +6431,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -6630,6 +6620,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -7096,6 +7093,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -7429,13 +7436,13 @@ } }, "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" @@ -7539,6 +7546,25 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -8226,6 +8252,15 @@ "node": ">=12.0.0" } }, + "node_modules/cron/node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8333,6 +8368,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8456,16 +8509,16 @@ } }, "node_modules/docker-modem": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", - "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", - "ssh2": "^1.11.0" + "ssh2": "^1.15.0" }, "engines": { "node": ">= 8.0" @@ -8487,15 +8540,19 @@ } }, "node_modules/dockerode": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz", - "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.5.tgz", + "integrity": "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.1.2", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" @@ -8524,16 +8581,16 @@ } }, "node_modules/dockerode/node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.0.0" + "tar-stream": "^2.1.4" } }, "node_modules/dockerode/node_modules/tar-stream": { @@ -8553,6 +8610,20 @@ "node": ">=6" } }, + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -9029,14 +9100,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -9047,7 +9118,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -9060,28 +9131,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" @@ -9090,7 +9161,7 @@ "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/globals": { @@ -9630,6 +9701,29 @@ "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/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9978,9 +10072,9 @@ } }, "node_modules/geo-tz": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.3.tgz", - "integrity": "sha512-zzF0hjqLl+1n5tXDCxwdS/BmF+N1TdQc6rbubh6PO6/9DtntX/yBox1Ti0q24MrjajWG0fSv0gv2w6Zff/kmeA==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.4.tgz", + "integrity": "sha512-xayeOC05wgy6JATU/k7GFHTMfSimzL1Fi3KSzt2GqvEnP1ZFXyQ9V4VAiTrTYhZSmRr0dbchZkximSegHZNUfA==", "license": "MIT", "dependencies": { "@turf/boolean-point-in-polygon": "^7.1.0", @@ -10045,13 +10139,13 @@ } }, "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10328,6 +10422,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -10383,9 +10490,22 @@ } }, "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==", + "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/hosted-git-info/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==", "dev": true, "license": "ISC" }, @@ -10553,13 +10673,29 @@ } }, "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "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": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.0.0.tgz", + "integrity": "sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/inflight": { @@ -10703,16 +10839,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" @@ -10733,6 +10869,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10840,6 +10992,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -11069,6 +11234,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", + "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -11076,6 +11261,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -11108,6 +11300,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11118,6 +11320,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kysely": { "version": "0.27.6", "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", @@ -11442,9 +11654,9 @@ } }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "license": "MIT", "engines": { "node": ">=12" @@ -12042,9 +12254,9 @@ } }, "node_modules/nestjs-cls": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-5.4.1.tgz", - "integrity": "sha512-hm0CQE6hRjr07yqSTcj50eQwNuZhlOHocX1W0CHpsYo9aDqsEysGkshegvAlC/4NVedTeAU6J0dVjAvNPTMrjQ==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-5.4.2.tgz", + "integrity": "sha512-KQPOhD7ya82gSEc+XDwFKERPMaWK95bzV4E2pLmx8oC1hfMNuVc4dkWmEKJiu+o0hCWP/v51iWNgOGHKnJ9Raw==", "license": "MIT", "engines": { "node": ">=18" @@ -12229,26 +12441,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, - "license": "ISC", - "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": { @@ -12309,6 +12513,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/oidc-token-hash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", @@ -12363,6 +12577,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", @@ -12490,16 +12721,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "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" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12564,6 +12785,105 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/patch-package/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13345,25 +13665,24 @@ } }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.1.0" } }, "node_modules/react-email": { @@ -13575,114 +13894,86 @@ "pify": "^2.3.0" } }, - "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==", + "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": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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==", + "node_modules/read-package-up/node_modules/type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "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.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.2.0.tgz", + "integrity": "sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.0.0", + "type-fest": "^4.37.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/readable-stream": { @@ -13829,25 +14120,29 @@ } }, "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/repeat-string": { @@ -14163,11 +14458,10 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "license": "MIT", - "peer": true + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/schema-utils": { "version": "3.3.0", @@ -14296,6 +14590,24 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -14575,6 +14887,16 @@ "node": ">=18" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -14798,9 +15120,9 @@ } }, "node_modules/sql-formatter": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.5.1.tgz", - "integrity": "sha512-H3XfFRpK8LybkU2mD2Vj3AF35YcfviwUuy3yl98Xp7DTneJJVB40L654DpHYIRctASNIQPoLo7+yCboOfkOpWA==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.5.2.tgz", + "integrity": "sha512-+9xZgiv1DP/c7GxkkBUHRZOf4j35gquVdwEm0rg16qKRYeFkv1+/vEeO13fsUbbz06KUotIyASJ+hyau8LM8Kg==", "dev": true, "license": "MIT", "dependencies": { @@ -15051,16 +15373,19 @@ } }, "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "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.0" + "min-indent": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -15253,14 +15578,14 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -15725,21 +16050,21 @@ } }, "node_modules/testcontainers": { - "version": "10.21.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.21.0.tgz", - "integrity": "sha512-avzqNfP0HyHaOPTztHbGnHycZWzWaeUwSPnJGCYin2LB+n/tlGjAzExVrkIaCSBwIjFkMlufEYuKWzGc8mupDQ==", + "version": "10.23.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.23.0.tgz", + "integrity": "sha512-sZeij9mAyR9ixlaAmxU/DNb5LQ2duGCBDVjLaI975QGsX3sWatsBMDr4rqnP3IBemLynp+azZBMEfw75YsXMMg==", "dev": true, "license": "MIT", "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.29", + "@types/dockerode": "^3.3.35", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", - "dockerode": "^3.3.5", - "get-port": "^5.1.1", + "dockerode": "^4.0.4", + "get-port": "^7.1.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", @@ -16287,6 +16612,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/ua-is-frozen": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", @@ -16393,6 +16741,19 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "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/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -16549,9 +16910,9 @@ } }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -16639,9 +17000,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "dev": true, "license": "MIT", "dependencies": { @@ -17160,31 +17521,31 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "debug": "^4.4.0", - "expect-type": "^1.1.0", + "expect-type": "^1.2.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "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.9", + "vite-node": "3.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17200,8 +17561,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/server/package.json b/server/package.json index d600fbad9a..c7a64594ff 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.130.3", + "version": "1.131.3", "description": "", "author": "", "private": true, @@ -23,8 +23,8 @@ "test:medium": "vitest --config test/vitest.config.medium.mjs", "typeorm": "typeorm", "lifecycle": "node ./dist/utils/lifecycle.js", - "typeorm:migrations:create": "typeorm migration:create", - "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/bin/database.js", + "migrations:generate": "node ./dist/bin/migrations.js generate", + "migrations:create": "node ./dist/bin/migrations.js create", "typeorm:migrations:run": "typeorm migration:run -d ./dist/bin/database.js", "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js", "typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'", @@ -32,7 +32,8 @@ "kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts", "sync:open-api": "node ./dist/bin/sync-open-api.js", "sync:sql": "node ./dist/bin/sync-sql.js", - "email:dev": "email dev -p 3050 --dir src/emails" + "email:dev": "email dev -p 3050 --dir src/emails", + "postinstall": "[ \"$npm_config_global\" != \"true\" ] && patch-package || true" }, "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -45,7 +46,7 @@ "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", "@opentelemetry/auto-instrumentations-node": "^0.57.0", - "@opentelemetry/context-async-hooks": "^1.24.0", + "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/exporter-prometheus": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0", "@react-email/components": "^0.0.34", @@ -81,6 +82,7 @@ "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", + "react-dom": "^19.0.0", "react-email": "^3.0.4", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -114,7 +116,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", @@ -123,17 +125,16 @@ "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", "kysely-codegen": "^0.18.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", + "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -143,6 +144,7 @@ "testcontainers": "^10.18.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", + "typescript-eslint": "^8.28.0", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", "vite-tsconfig-paths": "^5.0.0", diff --git a/server/patches/postgres+3.4.5.patch b/server/patches/postgres+3.4.5.patch new file mode 100644 index 0000000000..d879416978 --- /dev/null +++ b/server/patches/postgres+3.4.5.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/postgres/cf/src/connection.js b/node_modules/postgres/cf/src/connection.js +index ee8b1e6..d03b9dd 100644 +--- a/node_modules/postgres/cf/src/connection.js ++++ b/node_modules/postgres/cf/src/connection.js +@@ -387,6 +387,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose + } + + function queryError(query, err) { ++ if (!query || typeof query !== 'object') throw err ++ + 'query' in err || 'parameters' in err || Object.defineProperties(err, { + stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, + query: { value: query.string, enumerable: options.debug }, +diff --git a/node_modules/postgres/cjs/src/connection.js b/node_modules/postgres/cjs/src/connection.js +index f7f58d1..8a37571 100644 +--- a/node_modules/postgres/cjs/src/connection.js ++++ b/node_modules/postgres/cjs/src/connection.js +@@ -385,6 +385,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose + } + + function queryError(query, err) { ++ if (!query || typeof query !== 'object') throw err ++ + 'query' in err || 'parameters' in err || Object.defineProperties(err, { + stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, + query: { value: query.string, enumerable: options.debug }, +diff --git a/node_modules/postgres/src/connection.js b/node_modules/postgres/src/connection.js +index 97cc97e..58f5298 100644 +--- a/node_modules/postgres/src/connection.js ++++ b/node_modules/postgres/src/connection.js +@@ -385,6 +385,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose + } + + function queryError(query, err) { ++ if (!query || typeof query !== 'object') throw err ++ + 'query' in err || 'parameters' in err || Object.defineProperties(err, { + stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, + query: { value: query.string, enumerable: options.debug }, diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts new file mode 100644 index 0000000000..b553ff7fa7 --- /dev/null +++ b/server/src/bin/migrations.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env node +process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich'; + +import { writeFileSync } from 'node:fs'; +import postgres from 'postgres'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import 'src/schema/tables'; +import { DatabaseTable, schemaDiff, schemaFromDatabase, schemaFromDecorators } from 'src/sql-tools'; + +const main = async () => { + const command = process.argv[2]; + const name = process.argv[3] || 'Migration'; + + switch (command) { + case 'debug': { + await debug(); + return; + } + + case 'create': { + create(name, [], []); + return; + } + + case 'generate': { + await generate(name); + return; + } + + default: { + console.log(`Usage: + node dist/bin/migrations.js create + node dist/bin/migrations.js generate +`); + } + } +}; + +const debug = async () => { + const { up, down } = await compare(); + const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n'); + const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n'); + writeFileSync('./migrations.sql', upSql + '\n\n' + downSql); + console.log('Wrote migrations.sql'); +}; + +const generate = async (name: string) => { + const { up, down } = await compare(); + if (up.items.length === 0) { + console.log('No changes detected'); + return; + } + create(name, up.asSql(), down.asSql()); +}; + +const create = (name: string, up: string[], down: string[]) => { + const timestamp = Date.now(); + const filename = `${timestamp}-${name}.ts`; + const fullPath = `./src/${filename}`; + writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down })); + console.log(`Wrote ${fullPath}`); +}; + +const compare = async () => { + const configRepository = new ConfigRepository(); + const { database } = configRepository.getEnv(); + const db = postgres(database.config.kysely); + + const source = schemaFromDecorators(); + const target = await schemaFromDatabase(db, {}); + + console.log(source.warnings.join('\n')); + + const isIncluded = (table: DatabaseTable) => source.tables.some(({ name }) => table.name === name); + target.tables = target.tables.filter((table) => isIncluded(table)); + + const up = schemaDiff(source, target, { ignoreExtraTables: true }); + const down = schemaDiff(target, source, { ignoreExtraTables: false }); + + return { up, down }; +}; + +type MigrationProps = { + name: string; + timestamp: number; + up: string[]; + down: string[]; +}; + +const asMigration = (type: 'kysely' | 'typeorm', options: MigrationProps) => + type === 'typeorm' ? asTypeOrmMigration(options) : asKyselyMigration(options); + +const asTypeOrmMigration = ({ timestamp, name, up, down }: MigrationProps) => { + const upSql = up.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n'); + const downSql = down.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n'); + + return `import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ${name}${timestamp} implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { +${upSql} + } + + public async down(queryRunner: QueryRunner): Promise { +${downSql} + } +} +`; +}; + +const asKyselyMigration = ({ up, down }: MigrationProps) => { + const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n'); + const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n'); + + return `import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { +${upSql} +} + +export async function down(db: Kysely): Promise { +${downSql} +} +`; +}; + +main() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error(error); + console.log('Something went wrong'); + process.exit(1); + }); diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 61c19c02fb..8886334f47 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -121,7 +121,7 @@ class SqlGenerator { for (const key of this.getPropertyNames(instance)) { const target = instance[key]; - if (!(target instanceof Function)) { + if (!(typeof target === 'function')) { continue; } diff --git a/server/src/config.ts b/server/src/config.ts index e7f3d4b8b6..566adbd693 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -12,7 +12,7 @@ import { VideoCodec, VideoContainer, } from 'src/enum'; -import { ConcurrentQueueName, ImageOptions } from 'src/types'; +import { ConcurrentQueueName, FullsizeImageOptions, ImageOptions } from 'src/types'; export interface SystemConfig { backup: { @@ -112,6 +112,7 @@ export interface SystemConfig { preview: ImageOptions; colorspace: Colorspace; extractEmbedded: boolean; + fullsize: FullsizeImageOptions; }; newVersionCheck: { enabled: boolean; @@ -281,6 +282,11 @@ export const defaults = Object.freeze({ }, colorspace: Colorspace.P3, extractEmbedded: false, + fullsize: { + enabled: false, + format: ImageFormat.JPEG, + quality: 80, + }, }, newVersionCheck: { enabled: true, diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 3d2845690d..b2c9397580 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -10,12 +10,13 @@ import { Post, Put, Query, + Req, Res, UploadedFiles, UseInterceptors, } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { NextFunction, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; import { EndpointLifecycle } from 'src/decorators'; import { AssetBulkUploadCheckResponseDto, @@ -28,6 +29,7 @@ import { AssetMediaCreateDto, AssetMediaOptionsDto, AssetMediaReplaceDto, + AssetMediaSize, CheckExistingAssetsDto, UploadFieldName, } from 'src/dtos/asset-media.dto'; @@ -39,7 +41,7 @@ import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.inte import { LoggingRepository } from 'src/repositories/logging.repository'; import { AssetMediaService } from 'src/services/asset-media.service'; import { UploadFiles } from 'src/types'; -import { sendFile } from 'src/utils/file'; +import { ImmichFileResponse, sendFile } from 'src/utils/file'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; @ApiTags('Assets') @@ -123,10 +125,34 @@ export class AssetMediaController { @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Query() dto: AssetMediaOptionsDto, + @Req() req: Request, @Res() res: Response, @Next() next: NextFunction, ) { - await sendFile(res, next, () => this.service.viewThumbnail(auth, id, dto), this.logger); + const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto); + + if (viewThumbnailRes instanceof ImmichFileResponse) { + await sendFile(res, next, () => Promise.resolve(viewThumbnailRes), this.logger); + } else { + // viewThumbnailRes is a AssetMediaRedirectResponse + // which redirects to the original asset or a specific size to make better use of caching + const { targetSize } = viewThumbnailRes; + const [reqPath, reqSearch] = req.url.split('?'); + let redirPath: string; + const redirSearchParams = new URLSearchParams(reqSearch); + if (targetSize === 'original') { + // relative path to this.downloadAsset + redirPath = 'original'; + redirSearchParams.delete('size'); + } else if (Object.values(AssetMediaSize).includes(targetSize)) { + redirPath = reqPath; + redirSearchParams.set('size', targetSize); + } else { + throw new Error('Invalid targetSize: ' + targetSize); + } + const finalRedirPath = redirPath + '?' + redirSearchParams.toString(); + return res.redirect(finalRedirPath); + } } @Get(':id/video/playback') diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 3160331dd4..4cbf963158 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -12,7 +12,7 @@ import { MoveRepository } from 'src/repositories/move.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFile } from 'src/utils/asset.util'; import { getConfig } from 'src/utils/config'; export interface MoveRequest { @@ -26,7 +26,7 @@ export interface MoveRequest { }; } -export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL; +export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL | AssetPathType.FULLSIZE; export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; let instance: StorageCore | null; @@ -117,8 +117,7 @@ export class StorageCore { async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { const { id: entityId, files } = asset; - const { thumbnailFile, previewFile } = getAssetFiles(files); - const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile; + const oldFile = getAssetFile(files, pathType); return this.moveFile({ entityId, pathType, @@ -277,6 +276,9 @@ export class StorageCore { case AssetPathType.ORIGINAL: { return this.assetRepository.update({ id, originalPath: newPath }); } + case AssetPathType.FULLSIZE: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FULLSIZE, path: newPath }); + } case AssetPathType.PREVIEW: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); } diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 85aade2c9b..ca6d1813e4 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -4,8 +4,9 @@ */ import type { ColumnType } from 'kysely'; -import { OnThisDayData } from 'src/entities/memory.entity'; import { AssetType, MemoryType, Permission, SyncEntityType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; +import { OnThisDayData } from 'src/types'; export type ArrayType = ArrayTypeImpl extends (infer U)[] ? U[] : ArrayTypeImpl; @@ -410,26 +411,6 @@ export interface UserMetadata { value: Json; } -export interface Users { - createdAt: Generated; - deletedAt: Timestamp | null; - email: string; - id: Generated; - isAdmin: Generated; - name: Generated; - oauthId: Generated; - password: Generated; - profileChangedAt: Generated; - profileImagePath: Generated; - quotaSizeInBytes: Int8 | null; - quotaUsageInBytes: Generated; - shouldChangePassword: Generated; - status: Generated; - storageLabel: string | null; - updatedAt: Generated; - updateId: Generated; -} - export interface UsersAudit { id: Generated; userId: string; @@ -495,7 +476,7 @@ export interface DB { tags_closure: TagsClosure; typeorm_metadata: TypeormMetadata; user_metadata: UserMetadata; - users: Users; + users: UserTable; users_audit: UsersAudit; 'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat; version_history: VersionHistory; diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index c62857da65..8837138599 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -4,6 +4,11 @@ import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { + /** + * An full-sized image extracted/converted from non-web-friendly formats like RAW/HIF. + * or otherwise the original image itself. + */ + FULLSIZE = 'fullsize', PREVIEW = 'preview', THUMBNAIL = 'thumbnail', } diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 3589331c78..e0b5c9b779 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -191,6 +191,11 @@ export class SmartSearchDto extends BaseSearchDto { @IsNotEmpty() query!: string; + @IsString() + @IsNotEmpty() + @Optional() + language?: string; + @IsInt() @Min(1) @Type(() => Number) diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 6b51c015b7..eaef40a5e1 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -531,6 +531,24 @@ class SystemConfigGeneratedImageDto { size!: number; } +class SystemConfigGeneratedFullsizeImageDto { + @IsBoolean() + @Type(() => Boolean) + @ApiProperty({ type: 'boolean' }) + enabled!: boolean; + + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + format!: ImageFormat; + + @IsInt() + @Min(1) + @Max(100) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + quality!: number; +} + export class SystemConfigImageDto { @Type(() => SystemConfigGeneratedImageDto) @ValidateNested() @@ -542,6 +560,11 @@ export class SystemConfigImageDto { @IsObject() preview!: SystemConfigGeneratedImageDto; + @Type(() => SystemConfigGeneratedFullsizeImageDto) + @ValidateNested() + @IsObject() + fullsize!: SystemConfigGeneratedFullsizeImageDto; + @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) colorspace!: Colorspace; diff --git a/server/src/entities/activity.entity.ts b/server/src/entities/activity.entity.ts deleted file mode 100644 index dabb371977..0000000000 --- a/server/src/entities/activity.entity.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { UserEntity } from 'src/entities/user.entity'; -import { - Check, - Column, - CreateDateColumn, - Entity, - Index, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; - -@Entity('activity') -@Index('IDX_activity_like', ['assetId', 'userId', 'albumId'], { unique: true, where: '("isLiked" = true)' }) -@Check(`("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)`) -export class ActivityEntity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ type: 'timestamptz' }) - updatedAt!: Date; - - @Index('IDX_activity_update_id') - @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) - updateId?: string; - - @Column() - albumId!: string; - - @Column() - userId!: string; - - @Column({ nullable: true, type: 'uuid' }) - assetId!: string | null; - - @Column({ type: 'text', default: null }) - comment!: string | null; - - @Column({ type: 'boolean', default: false }) - isLiked!: boolean; - - @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) - asset!: AssetEntity | null; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - user!: UserEntity; - - @ManyToOne(() => AlbumEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - album!: AlbumEntity; -} diff --git a/server/src/entities/album-user.entity.ts b/server/src/entities/album-user.entity.ts index e75b3cd43e..7950ffab7d 100644 --- a/server/src/entities/album-user.entity.ts +++ b/server/src/entities/album-user.entity.ts @@ -1,27 +1,11 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { UserEntity } from 'src/entities/user.entity'; import { AlbumUserRole } from 'src/enum'; -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -@Entity('albums_shared_users_users') -// Pre-existing indices from original album <--> user ManyToMany mapping -@Index('IDX_427c350ad49bd3935a50baab73', ['album']) -@Index('IDX_f48513bf9bccefd6ff3ad30bd0', ['user']) export class AlbumUserEntity { - @PrimaryColumn({ type: 'uuid', name: 'albumsId' }) albumId!: string; - - @PrimaryColumn({ type: 'uuid', name: 'usersId' }) userId!: string; - - @JoinColumn({ name: 'albumsId' }) - @ManyToOne(() => AlbumEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) album!: AlbumEntity; - - @JoinColumn({ name: 'usersId' }) - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) user!: UserEntity; - - @Column({ type: 'varchar', default: AlbumUserRole.EDITOR }) role!: AlbumUserRole; } diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts index 4cd7c82394..946c807a1a 100644 --- a/server/src/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -3,69 +3,22 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; import { AssetOrder } from 'src/enum'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; -@Entity('albums') export class AlbumEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) owner!: UserEntity; - - @Column() ownerId!: string; - - @Column({ default: 'Untitled Album' }) albumName!: string; - - @Column({ type: 'text', default: '' }) description!: string; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @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; - - @ManyToOne(() => AssetEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) albumThumbnailAsset!: AssetEntity | null; - - @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true }) albumThumbnailAssetId!: string | null; - - @OneToMany(() => AlbumUserEntity, ({ album }) => album, { cascade: true, onDelete: 'CASCADE' }) albumUsers!: AlbumUserEntity[]; - - @ManyToMany(() => AssetEntity, (asset) => asset.albums) - @JoinTable({ synchronize: false }) assets!: AssetEntity[]; - - @OneToMany(() => SharedLinkEntity, (link) => link.album) sharedLinks!: SharedLinkEntity[]; - - @Column({ default: true }) isActivityEnabled!: boolean; - - @Column({ type: 'varchar', default: AssetOrder.DESC }) order!: AssetOrder; } diff --git a/server/src/entities/api-key.entity.ts b/server/src/entities/api-key.entity.ts deleted file mode 100644 index f59bf0d918..0000000000 --- a/server/src/entities/api-key.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UserEntity } from 'src/entities/user.entity'; -import { Permission } from 'src/enum'; -import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('api_keys') -export class APIKeyEntity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column() - name!: string; - - @Column({ select: false }) - key?: string; - - @ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) - user?: UserEntity; - - @Column() - userId!: string; - - @Column({ array: true, type: 'varchar' }) - permissions!: Permission[]; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @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-audit.entity.ts b/server/src/entities/asset-audit.entity.ts deleted file mode 100644 index 0172d15ce6..0000000000 --- a/server/src/entities/asset-audit.entity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; - -@Entity('assets_audit') -export class AssetAuditEntity { - @PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) - id!: string; - - @Index('IDX_assets_audit_asset_id') - @Column({ type: 'uuid' }) - assetId!: string; - - @Index('IDX_assets_audit_owner_id') - @Column({ type: 'uuid' }) - ownerId!: string; - - @Index('IDX_assets_audit_deleted_at') - @CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) - deletedAt!: Date; -} diff --git a/server/src/entities/asset-face.entity.ts b/server/src/entities/asset-face.entity.ts index b556a8b7cf..dddb6b0f3f 100644 --- a/server/src/entities/asset-face.entity.ts +++ b/server/src/entities/asset-face.entity.ts @@ -2,55 +2,20 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { FaceSearchEntity } from 'src/entities/face-search.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SourceType } from 'src/enum'; -import { Column, Entity, Index, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; -@Entity('asset_faces', { synchronize: false }) -@Index('IDX_asset_faces_assetId_personId', ['assetId', 'personId']) -@Index(['personId', 'assetId']) export class AssetFaceEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column() assetId!: string; - - @Column({ nullable: true, type: 'uuid' }) personId!: string | null; - - @OneToOne(() => FaceSearchEntity, (faceSearchEntity) => faceSearchEntity.face, { cascade: ['insert'] }) faceSearch?: FaceSearchEntity; - - @Column({ default: 0, type: 'int' }) imageWidth!: number; - - @Column({ default: 0, type: 'int' }) imageHeight!: number; - - @Column({ default: 0, type: 'int' }) boundingBoxX1!: number; - - @Column({ default: 0, type: 'int' }) boundingBoxY1!: number; - - @Column({ default: 0, type: 'int' }) boundingBoxX2!: number; - - @Column({ default: 0, type: 'int' }) boundingBoxY2!: number; - - @Column({ default: SourceType.MACHINE_LEARNING, type: 'enum', enum: SourceType }) sourceType!: SourceType; - - @ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) asset!: AssetEntity; - - @ManyToOne(() => PersonEntity, (person) => person.faces, { - onDelete: 'SET NULL', - onUpdate: 'CASCADE', - nullable: true, - }) person!: PersonEntity | null; - - @Column({ type: 'timestamptz' }) deletedAt!: Date | null; } diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts index 09f96e849d..3bd80784b6 100644 --- a/server/src/entities/asset-files.entity.ts +++ b/server/src/entities/asset-files.entity.ts @@ -1,42 +1,13 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { AssetFileType } from 'src/enum'; -import { - Column, - CreateDateColumn, - Entity, - Index, - ManyToOne, - PrimaryGeneratedColumn, - Unique, - UpdateDateColumn, -} from 'typeorm'; -@Unique('UQ_assetId_type', ['assetId', 'type']) -@Entity('asset_files') export class AssetFileEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Index('IDX_asset_files_assetId') - @Column() assetId!: string; - - @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) asset?: AssetEntity; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @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; - - @Column() path!: string; } diff --git a/server/src/entities/asset-job-status.entity.ts b/server/src/entities/asset-job-status.entity.ts index 353055df43..2cccfcab3a 100644 --- a/server/src/entities/asset-job-status.entity.ts +++ b/server/src/entities/asset-job-status.entity.ts @@ -1,27 +1,11 @@ import { AssetEntity } from 'src/entities/asset.entity'; -import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -@Entity('asset_job_status') export class AssetJobStatusEntity { - @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - @JoinColumn() asset!: AssetEntity; - - @PrimaryColumn() assetId!: string; - - @Column({ type: 'timestamptz', nullable: true }) facesRecognizedAt!: Date | null; - - @Column({ type: 'timestamptz', nullable: true }) metadataExtractedAt!: Date | null; - - @Column({ type: 'timestamptz', nullable: true }) duplicatesDetectedAt!: Date | null; - - @Column({ type: 'timestamptz', nullable: true }) previewAt!: Date | null; - - @Column({ type: 'timestamptz', nullable: true }) thumbnailAt!: Date | null; } diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index b2589e1231..836fc409af 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -6,7 +6,6 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { LibraryEntity } from 'src/entities/library.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { StackEntity } from 'src/entities/stack.entity'; @@ -16,171 +15,49 @@ import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { TimeBucketSize } from 'src/repositories/asset.repository'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; import { anyUuid, asUuid } from 'src/utils/database'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - JoinColumn, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - OneToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; -@Entity('assets') -// Checksums must be unique per user and library -@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'checksum'], { - unique: true, - where: '"libraryId" IS NULL', -}) -@Index('UQ_assets_owner_library_checksum' + '', ['owner', 'library', 'checksum'], { - unique: true, - where: '"libraryId" IS NOT NULL', -}) -@Index('idx_local_date_time', { synchronize: false }) -@Index('idx_local_date_time_month', { synchronize: false }) -@Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId']) -@Index('IDX_asset_id_stackId', ['id', 'stackId']) -@Index('idx_originalFileName_trigram', { synchronize: false }) -// For all assets, each originalpath must be unique per user and library export class AssetEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column() deviceAssetId!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) owner!: UserEntity; - - @Column() ownerId!: string; - - @ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - library?: LibraryEntity | null; - - @Column({ nullable: true }) libraryId?: string | null; - - @Column() deviceId!: string; - - @Column() type!: AssetType; - - @Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE }) status!: AssetStatus; - - @Column() originalPath!: string; - - @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset) files!: AssetFileEntity[]; - - @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; - - @Column({ type: 'varchar', nullable: true, default: '' }) encodedVideoPath!: string | null; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @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; - - @Index('idx_asset_file_created_at') - @Column({ type: 'timestamptz', nullable: true, default: null }) fileCreatedAt!: Date; - - @Column({ type: 'timestamptz', nullable: true, default: null }) localDateTime!: Date; - - @Column({ type: 'timestamptz', nullable: true, default: null }) fileModifiedAt!: Date; - - @Column({ type: 'boolean', default: false }) isFavorite!: boolean; - - @Column({ type: 'boolean', default: false }) isArchived!: boolean; - - @Column({ type: 'boolean', default: false }) isExternal!: boolean; - - @Column({ type: 'boolean', default: false }) isOffline!: boolean; - - @Column({ type: 'bytea' }) - @Index() checksum!: Buffer; // sha1 checksum - - @Column({ type: 'varchar', nullable: true }) duration!: string | null; - - @Column({ type: 'boolean', default: true }) isVisible!: boolean; - - @ManyToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' }) - @JoinColumn() livePhotoVideo!: AssetEntity | null; - - @Column({ nullable: true }) livePhotoVideoId!: string | null; - - @Column({ type: 'varchar' }) - @Index() originalFileName!: string; - - @Column({ type: 'varchar', nullable: true }) sidecarPath!: string | null; - - @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset) exifInfo?: ExifEntity; - - @OneToOne(() => SmartSearchEntity, (smartSearchEntity) => smartSearchEntity.asset) smartSearch?: SmartSearchEntity; - - @ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true }) - @JoinTable({ name: 'tag_asset', synchronize: false }) tags!: TagEntity[]; - - @ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true }) - @JoinTable({ name: 'shared_link__asset' }) sharedLinks!: SharedLinkEntity[]; - - @ManyToMany(() => AlbumEntity, (album) => album.assets, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) albums?: AlbumEntity[]; - - @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.asset) faces!: AssetFaceEntity[]; - - @Column({ nullable: true }) stackId?: string | null; - - @ManyToOne(() => StackEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) - @JoinColumn() stack?: StackEntity | null; - - @OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true }) jobStatus?: AssetJobStatusEntity; - - @Index('IDX_assets_duplicateId') - @Column({ type: 'uuid', nullable: true }) duplicateId!: string | null; } diff --git a/server/src/entities/audit.entity.ts b/server/src/entities/audit.entity.ts deleted file mode 100644 index 7f51e17585..0000000000 --- a/server/src/entities/audit.entity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DatabaseAction, EntityType } from 'src/enum'; -import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity('audit') -@Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt']) -export class AuditEntity { - @PrimaryGeneratedColumn('increment') - id!: number; - - @Column() - entityType!: EntityType; - - @Column({ type: 'uuid' }) - entityId!: string; - - @Column() - action!: DatabaseAction; - - @Column({ type: 'uuid' }) - ownerId!: string; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; -} diff --git a/server/src/entities/exif.entity.ts b/server/src/entities/exif.entity.ts index 5b402109a5..75064b7917 100644 --- a/server/src/entities/exif.entity.ts +++ b/server/src/entities/exif.entity.ts @@ -1,111 +1,36 @@ import { AssetEntity } from 'src/entities/asset.entity'; -import { Index, JoinColumn, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; -import { Column } from 'typeorm/decorator/columns/Column.js'; -import { Entity } from 'typeorm/decorator/entity/Entity.js'; -@Entity('exif') export class ExifEntity { - @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) - @JoinColumn() asset?: AssetEntity; - - @PrimaryColumn() assetId!: string; - - @UpdateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) updatedAt?: Date; - - @Index('IDX_asset_exif_update_id') - @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) updateId?: string; - - /* General info */ - @Column({ type: 'text', default: '' }) description!: string; // or caption - - @Column({ type: 'integer', nullable: true }) exifImageWidth!: number | null; - - @Column({ type: 'integer', nullable: true }) exifImageHeight!: number | null; - - @Column({ type: 'bigint', nullable: true }) fileSizeInByte!: number | null; - - @Column({ type: 'varchar', nullable: true }) orientation!: string | null; - - @Column({ type: 'timestamptz', nullable: true }) dateTimeOriginal!: Date | null; - - @Column({ type: 'timestamptz', nullable: true }) modifyDate!: Date | null; - - @Column({ type: 'varchar', nullable: true }) timeZone!: string | null; - - @Column({ type: 'float', nullable: true }) latitude!: number | null; - - @Column({ type: 'float', nullable: true }) longitude!: number | null; - - @Column({ type: 'varchar', nullable: true }) projectionType!: string | null; - - @Index('exif_city') - @Column({ type: 'varchar', nullable: true }) city!: string | null; - - @Index('IDX_live_photo_cid') - @Column({ type: 'varchar', nullable: true }) livePhotoCID!: string | null; - - @Index('IDX_auto_stack_id') - @Column({ type: 'varchar', nullable: true }) autoStackId!: string | null; - - @Column({ type: 'varchar', nullable: true }) state!: string | null; - - @Column({ type: 'varchar', nullable: true }) country!: string | null; - - /* Image info */ - @Column({ type: 'varchar', nullable: true }) make!: string | null; - - @Column({ type: 'varchar', nullable: true }) model!: string | null; - - @Column({ type: 'varchar', nullable: true }) lensModel!: string | null; - - @Column({ type: 'float8', nullable: true }) fNumber!: number | null; - - @Column({ type: 'float8', nullable: true }) focalLength!: number | null; - - @Column({ type: 'integer', nullable: true }) iso!: number | null; - - @Column({ type: 'varchar', nullable: true }) exposureTime!: string | null; - - @Column({ type: 'varchar', nullable: true }) profileDescription!: string | null; - - @Column({ type: 'varchar', nullable: true }) colorspace!: string | null; - - @Column({ type: 'integer', nullable: true }) bitsPerSample!: number | null; - - @Column({ type: 'integer', nullable: true }) rating!: number | null; - - /* Video info */ - @Column({ type: 'float8', nullable: true }) fps?: number | null; } diff --git a/server/src/entities/face-search.entity.ts b/server/src/entities/face-search.entity.ts index e907ba6c9e..701fd9e580 100644 --- a/server/src/entities/face-search.entity.ts +++ b/server/src/entities/face-search.entity.ts @@ -1,16 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -@Entity('face_search', { synchronize: false }) export class FaceSearchEntity { - @OneToOne(() => AssetFaceEntity, { onDelete: 'CASCADE', nullable: true }) - @JoinColumn({ name: 'faceId', referencedColumnName: 'id' }) face?: AssetFaceEntity; - - @PrimaryColumn() faceId!: string; - - @Index('face_index', { synchronize: false }) - @Column({ type: 'float4', array: true }) embedding!: string; } diff --git a/server/src/entities/geodata-places.entity.ts b/server/src/entities/geodata-places.entity.ts index eb32d1b99b..aad6c38dda 100644 --- a/server/src/entities/geodata-places.entity.ts +++ b/server/src/entities/geodata-places.entity.ts @@ -1,73 +1,13 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; - -@Entity('geodata_places', { synchronize: false }) export class GeodataPlacesEntity { - @PrimaryColumn({ type: 'integer' }) id!: number; - - @Column({ type: 'varchar', length: 200 }) name!: string; - - @Column({ type: 'float' }) longitude!: number; - - @Column({ type: 'float' }) latitude!: number; - - @Column({ type: 'char', length: 2 }) countryCode!: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) admin1Code!: string; - - @Column({ type: 'varchar', length: 80, nullable: true }) admin2Code!: string; - - @Column({ type: 'varchar', nullable: true }) admin1Name!: string; - - @Column({ type: 'varchar', nullable: true }) admin2Name!: string; - - @Column({ type: 'varchar', nullable: true }) alternateNames!: string; - - @Column({ type: 'date' }) - modificationDate!: Date; -} - -@Entity('geodata_places_tmp', { synchronize: false }) -export class GeodataPlacesTempEntity { - @PrimaryColumn({ type: 'integer' }) - id!: number; - - @Column({ type: 'varchar', length: 200 }) - name!: string; - - @Column({ type: 'float' }) - longitude!: number; - - @Column({ type: 'float' }) - latitude!: number; - - @Column({ type: 'char', length: 2 }) - countryCode!: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) - admin1Code!: string; - - @Column({ type: 'varchar', length: 80, nullable: true }) - admin2Code!: string; - - @Column({ type: 'varchar', nullable: true }) - admin1Name!: string; - - @Column({ type: 'varchar', nullable: true }) - admin2Name!: string; - - @Column({ type: 'varchar', nullable: true }) - alternateNames!: string; - - @Column({ type: 'date' }) modificationDate!: Date; } diff --git a/server/src/entities/library.entity.ts b/server/src/entities/library.entity.ts deleted file mode 100644 index 0471661fca..0000000000 --- a/server/src/entities/library.entity.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { AssetEntity } from 'src/entities/asset.entity'; -import { UserEntity } from 'src/entities/user.entity'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - JoinTable, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; - -@Entity('libraries') -export class LibraryEntity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column() - name!: string; - - @OneToMany(() => AssetEntity, (asset) => asset.library) - @JoinTable() - assets!: AssetEntity[]; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) - owner?: UserEntity; - - @Column() - ownerId!: string; - - @Column('text', { array: true }) - importPaths!: string[]; - - @Column('text', { array: true }) - exclusionPatterns!: string[]; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @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; - - @Column({ type: 'timestamptz', nullable: true }) - refreshedAt!: Date | null; -} diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts deleted file mode 100644 index dafd7eb21c..0000000000 --- a/server/src/entities/memory.entity.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AssetEntity } from 'src/entities/asset.entity'; -import { UserEntity } from 'src/entities/user.entity'; -import { MemoryType } from 'src/enum'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - JoinTable, - ManyToMany, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; - -export type OnThisDayData = { year: number }; - -export interface MemoryData { - [MemoryType.ON_THIS_DAY]: OnThisDayData; -} - -@Entity('memories') -export class MemoryEntity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @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; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) - owner!: UserEntity; - - @Column() - ownerId!: string; - - @Column() - type!: T; - - @Column({ type: 'jsonb' }) - data!: MemoryData[T]; - - /** unless set to true, will be automatically deleted in the future */ - @Column({ default: false }) - isSaved!: boolean; - - /** memories are sorted in ascending order by this value */ - @Column({ type: 'timestamptz' }) - memoryAt!: Date; - - @Column({ type: 'timestamptz', nullable: true }) - showAt?: Date; - - @Column({ type: 'timestamptz', nullable: true }) - hideAt?: Date; - - /** when the user last viewed the memory */ - @Column({ type: 'timestamptz', nullable: true }) - seenAt?: Date; - - @ManyToMany(() => AssetEntity) - @JoinTable() - assets!: AssetEntity[]; -} diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index 7a998eaebe..0570d98edc 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -1,24 +1,9 @@ import { PathType } from 'src/enum'; -import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; -@Entity('move_history') -// path lock (per entity) -@Unique('UQ_entityId_pathType', ['entityId', 'pathType']) -// new path lock (global) -@Unique('UQ_newPath', ['newPath']) export class MoveEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column({ type: 'uuid' }) entityId!: string; - - @Column({ type: 'varchar' }) pathType!: PathType; - - @Column({ type: 'varchar' }) oldPath!: string; - - @Column({ type: 'varchar' }) newPath!: string; } diff --git a/server/src/entities/natural-earth-countries.entity.ts b/server/src/entities/natural-earth-countries.entity.ts index 0f97132045..50bce3e034 100644 --- a/server/src/entities/natural-earth-countries.entity.ts +++ b/server/src/entities/natural-earth-countries.entity.ts @@ -1,37 +1,7 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity('naturalearth_countries', { synchronize: false }) -export class NaturalEarthCountriesEntity { - @PrimaryGeneratedColumn('identity', { generatedIdentity: 'ALWAYS' }) - id!: number; - - @Column({ type: 'varchar', length: 50 }) - admin!: string; - - @Column({ type: 'varchar', length: 3 }) - admin_a3!: string; - - @Column({ type: 'varchar', length: 50 }) - type!: string; - - @Column({ type: 'polygon' }) - coordinates!: string; -} - -@Entity('naturalearth_countries_tmp', { synchronize: false }) export class NaturalEarthCountriesTempEntity { - @PrimaryGeneratedColumn('identity', { generatedIdentity: 'ALWAYS' }) id!: number; - - @Column({ type: 'varchar', length: 50 }) admin!: string; - - @Column({ type: 'varchar', length: 3 }) admin_a3!: string; - - @Column({ type: 'varchar', length: 50 }) type!: string; - - @Column({ type: 'polygon' }) coordinates!: string; } diff --git a/server/src/entities/partner-audit.entity.ts b/server/src/entities/partner-audit.entity.ts deleted file mode 100644 index a731e017dc..0000000000 --- a/server/src/entities/partner-audit.entity.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 5326757736..0000000000 --- a/server/src/entities/partner.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserEntity } from 'src/entities/user.entity'; -import { - Column, - CreateDateColumn, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryColumn, - UpdateDateColumn, -} from 'typeorm'; - -/** @deprecated delete after coming up with a migration workflow for kysely */ -@Entity('partners') -export class PartnerEntity { - @PrimaryColumn('uuid') - sharedById!: string; - - @PrimaryColumn('uuid') - sharedWithId!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', eager: true }) - @JoinColumn({ name: 'sharedById' }) - sharedBy!: UserEntity; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', eager: true }) - @JoinColumn({ name: 'sharedWithId' }) - sharedWith!: UserEntity; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @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 5efa602cc8..6ea97b21bc 100644 --- a/server/src/entities/person.entity.ts +++ b/server/src/entities/person.entity.ts @@ -1,63 +1,20 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { - Check, - Column, - CreateDateColumn, - Entity, - Index, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; -@Entity('person') -@Check(`"birthDate" <= CURRENT_DATE`) export class PersonEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; - - @Index('IDX_person_update_id') - @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) updateId?: string; - - @Column() ownerId!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) owner!: UserEntity; - - @Column({ default: '' }) name!: string; - - @Column({ type: 'date', nullable: true }) birthDate!: Date | string | null; - - @Column({ default: '' }) thumbnailPath!: string; - - @Column({ nullable: true }) faceAssetId!: string | null; - - @ManyToOne(() => AssetFaceEntity, { onDelete: 'SET NULL', nullable: true }) faceAsset!: AssetFaceEntity | null; - - @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person) faces!: AssetFaceEntity[]; - - @Column({ default: false }) isHidden!: boolean; - - @Column({ default: false }) isFavorite!: boolean; - - @Column({ type: 'varchar', nullable: true, default: null }) color?: string | null; } diff --git a/server/src/entities/session.entity.ts b/server/src/entities/session.entity.ts index cb208c958e..45856ff2af 100644 --- a/server/src/entities/session.entity.ts +++ b/server/src/entities/session.entity.ts @@ -1,36 +1,16 @@ import { ExpressionBuilder } from 'kysely'; import { DB } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; -import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -@Entity('sessions') export class SessionEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column({ select: false }) token!: string; - - @Column() userId!: string; - - @ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user!: UserEntity; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @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; - - @Column({ default: '' }) deviceOS!: string; } diff --git a/server/src/entities/shared-link.entity.ts b/server/src/entities/shared-link.entity.ts index 1fed44b301..5ce0247be7 100644 --- a/server/src/entities/shared-link.entity.ts +++ b/server/src/entities/shared-link.entity.ts @@ -2,64 +2,21 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; import { SharedLinkType } from 'src/enum'; -import { - Column, - CreateDateColumn, - Entity, - Index, - ManyToMany, - ManyToOne, - PrimaryGeneratedColumn, - Unique, -} from 'typeorm'; -@Entity('shared_links') -@Unique('UQ_sharedlink_key', ['key']) export class SharedLinkEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column({ type: 'varchar', nullable: true }) description!: string | null; - - @Column({ type: 'varchar', nullable: true }) password!: string | null; - - @Column() userId!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) user!: UserEntity; - - @Index('IDX_sharedlink_key') - @Column({ type: 'bytea' }) key!: Buffer; // use to access the inidividual asset - - @Column() type!: SharedLinkType; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @Column({ type: 'timestamptz', nullable: true }) expiresAt!: Date | null; - - @Column({ type: 'boolean', default: false }) allowUpload!: boolean; - - @Column({ type: 'boolean', default: true }) allowDownload!: boolean; - - @Column({ type: 'boolean', default: true }) showExif!: boolean; - - @ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) assets!: AssetEntity[]; - - @Index('IDX_sharedlink_albumId') - @ManyToOne(() => AlbumEntity, (album) => album.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) album?: AlbumEntity; - - @Column({ type: 'varchar', nullable: true }) albumId!: string | null; } diff --git a/server/src/entities/smart-search.entity.ts b/server/src/entities/smart-search.entity.ts index 42245a17fb..e8a8f27cb1 100644 --- a/server/src/entities/smart-search.entity.ts +++ b/server/src/entities/smart-search.entity.ts @@ -1,16 +1,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; -import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -@Entity('smart_search', { synchronize: false }) export class SmartSearchEntity { - @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) - @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) asset?: AssetEntity; - - @PrimaryColumn() assetId!: string; - - @Index('clip_index', { synchronize: false }) - @Column({ type: 'float4', array: true }) embedding!: string; } diff --git a/server/src/entities/stack.entity.ts b/server/src/entities/stack.entity.ts index 883f5cf246..8b8fd94f38 100644 --- a/server/src/entities/stack.entity.ts +++ b/server/src/entities/stack.entity.ts @@ -1,28 +1,12 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; -@Entity('asset_stack') export class StackEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) owner!: UserEntity; - - @Column() ownerId!: string; - - @OneToMany(() => AssetEntity, (asset) => asset.stack) assets!: AssetEntity[]; - - @OneToOne(() => AssetEntity) - @JoinColumn() - //TODO: Add constraint to ensure primary asset exists in the assets array primaryAsset!: AssetEntity; - - @Column({ nullable: false }) primaryAssetId!: string; - assetCount?: number; } diff --git a/server/src/entities/sync-checkpoint.entity.ts b/server/src/entities/sync-checkpoint.entity.ts deleted file mode 100644 index 7c6818aba0..0000000000 --- a/server/src/entities/sync-checkpoint.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SessionEntity } from 'src/entities/session.entity'; -import { SyncEntityType } from 'src/enum'; -import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('session_sync_checkpoints') -export class SessionSyncCheckpointEntity { - @ManyToOne(() => SessionEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - session?: SessionEntity; - - @PrimaryColumn() - sessionId!: string; - - @PrimaryColumn({ type: 'varchar' }) - type!: SyncEntityType; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @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/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts deleted file mode 100644 index b024862ba5..0000000000 --- a/server/src/entities/system-metadata.entity.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { SystemConfig } from 'src/config'; -import { StorageFolder, SystemMetadataKey } from 'src/enum'; -import { DeepPartial } from 'src/types'; -import { Column, Entity, PrimaryColumn } from 'typeorm'; - -@Entity('system_metadata') -export class SystemMetadataEntity { - @PrimaryColumn({ type: 'varchar' }) - key!: T; - - @Column({ type: 'jsonb' }) - value!: SystemMetadata[T]; -} - -export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; -export type SystemFlags = { mountChecks: Record }; -export type MemoriesState = { - /** memories have already been created through this date */ - lastOnThisDayDate: string; -}; - -export interface SystemMetadata extends Record> { - [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean }; - [SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string }; - [SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; - [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; - [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial; - [SystemMetadataKey.SYSTEM_FLAGS]: DeepPartial; - [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; - [SystemMetadataKey.MEMORIES_STATE]: MemoriesState; -} diff --git a/server/src/entities/tag.entity.ts b/server/src/entities/tag.entity.ts index fcbde6c779..01235085a4 100644 --- a/server/src/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -1,58 +1,17 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { - Column, - CreateDateColumn, - Entity, - Index, - ManyToMany, - ManyToOne, - PrimaryGeneratedColumn, - Tree, - TreeChildren, - TreeParent, - Unique, - UpdateDateColumn, -} from 'typeorm'; -@Entity('tags') -@Unique(['userId', 'value']) -@Tree('closure-table') export class TagEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column() value!: string; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @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; - - @Column({ nullable: true }) parentId?: string; - - @TreeParent({ onDelete: 'CASCADE' }) parent?: TagEntity; - - @TreeChildren() children?: TagEntity[]; - - @ManyToOne(() => UserEntity, (user) => user.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user?: UserEntity; - - @Column() userId!: string; - - @ManyToMany(() => AssetEntity, (asset) => asset.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) assets?: AssetEntity[]; } diff --git a/server/src/entities/user-audit.entity.ts b/server/src/entities/user-audit.entity.ts deleted file mode 100644 index c29bc94d97..0000000000 --- a/server/src/entities/user-audit.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; - -@Entity('users_audit') -export class UserAuditEntity { - @PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) - id!: string; - - @Column({ type: 'uuid' }) - userId!: string; - - @Index('IDX_users_audit_deleted_at') - @CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) - deletedAt!: Date; -} diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 8c7a13ed0d..065f4deac3 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -2,25 +2,16 @@ import { UserEntity } from 'src/entities/user.entity'; import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { DeepPartial } from 'src/types'; import { HumanReadableSize } from 'src/utils/bytes'; -import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; export type UserMetadataItem = { key: T; value: UserMetadata[T]; }; -@Entity('user_metadata') export class UserMetadataEntity implements UserMetadataItem { - @PrimaryColumn({ type: 'uuid' }) userId!: string; - - @ManyToOne(() => UserEntity, (user) => user.metadata, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) user?: UserEntity; - - @PrimaryColumn({ type: 'varchar' }) key!: T; - - @Column({ type: 'jsonb' }) value!: UserMetadata[T]; } diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 5758e29098..5035f96274 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -2,82 +2,28 @@ import { ExpressionBuilder } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { DB } from 'src/db'; import { AssetEntity } from 'src/entities/asset.entity'; -import { TagEntity } from 'src/entities/tag.entity'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserStatus } from 'src/enum'; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - Index, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; -@Entity('users') -@Index('IDX_users_updated_at_asc_id_asc', ['updatedAt', 'id']) export class UserEntity { - @PrimaryGeneratedColumn('uuid') id!: string; - - @Column({ default: '' }) name!: string; - - @Column({ default: false }) isAdmin!: boolean; - - @Column({ unique: true }) email!: string; - - @Column({ type: 'varchar', unique: true, default: null }) storageLabel!: string | null; - - @Column({ default: '', select: false }) password?: string; - - @Column({ default: '' }) oauthId!: string; - - @Column({ default: '' }) profileImagePath!: string; - - @Column({ default: true }) shouldChangePassword!: boolean; - - @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; - - @DeleteDateColumn({ type: 'timestamptz' }) deletedAt!: Date | null; - - @Column({ type: 'varchar', default: UserStatus.ACTIVE }) status!: UserStatus; - - @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[]; - - @OneToMany(() => AssetEntity, (asset) => asset.owner) assets!: AssetEntity[]; - - @Column({ type: 'bigint', nullable: true }) quotaSizeInBytes!: number | null; - - @Column({ type: 'bigint', default: 0 }) quotaUsageInBytes!: number; - - @OneToMany(() => UserMetadataEntity, (metadata) => metadata.user) metadata!: UserMetadataEntity[]; - - @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) profileChangedAt!: Date; } diff --git a/server/src/entities/version-history.entity.ts b/server/src/entities/version-history.entity.ts deleted file mode 100644 index edccd9aed6..0000000000 --- a/server/src/entities/version-history.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity('version_history') -export class VersionHistoryEntity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt!: Date; - - @Column() - version!: string; -} diff --git a/server/src/enum.ts b/server/src/enum.ts index 6ebd1906f7..e5c6039be8 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -33,6 +33,10 @@ export enum AssetType { } export enum AssetFileType { + /** + * An full/large-size image extracted/converted from RAW photos + */ + FULLSIZE = 'fullsize', PREVIEW = 'preview', THUMBNAIL = 'thumbnail', } @@ -242,6 +246,7 @@ export enum ManualJobName { export enum AssetPathType { ORIGINAL = 'original', + FULLSIZE = 'fullsize', PREVIEW = 'preview', THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded_video', diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 6090c19cea..0c3f1dec81 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -201,19 +201,23 @@ order by -- AlbumRepository.getMetadataForIds select - "albums"."id" as "albumId", - min("assets"."localDateTime") as "startDate", - max("assets"."localDateTime") as "endDate", + "album_assets"."albumsId" as "albumId", + min( + ("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date + ) as "startDate", + max( + ("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date + ) as "endDate", + max("assets"."updatedAt") as "lastModifiedAssetTimestamp", count("assets"."id")::int as "assetCount" from - "albums" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" - inner join "assets" on "assets"."id" = "album_assets"."assetsId" + "assets" + inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" where - "albums"."id" in ($1) + "album_assets"."albumsId" in ($1) and "assets"."deletedAt" is null group by - "albums"."id" + "album_assets"."albumsId" -- AlbumRepository.getOwned select diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index e812b33f78..b2fdf976df 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -426,6 +426,9 @@ from where "assets"."ownerId" = $1::uuid and "assets"."isVisible" = $2 + and "assets"."fileCreatedAt" is not null + and "assets"."fileModifiedAt" is not null + and "assets"."localDateTime" is not null and "assets"."updatedAt" <= $3 and "assets"."id" > $4 order by @@ -456,6 +459,9 @@ from where "assets"."ownerId" = any ($1::uuid[]) and "assets"."isVisible" = $2 + and "assets"."fileCreatedAt" is not null + and "assets"."fileModifiedAt" is not null + and "assets"."localDateTime" is not null and "assets"."updatedAt" > $3 limit $4 diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 0fd1b233be..9fff558192 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -32,47 +32,7 @@ where "asset_stack"."ownerId" = $1 -- StackRepository.delete -select - *, - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "assets".*, - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "tags".* - from - "tags" - inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId" - where - "tag_asset"."assetsId" = "assets"."id" - ) as agg - ) as "tags", - to_json("exifInfo") as "exifInfo" - from - "assets" - inner join lateral ( - select - "exif".* - from - "exif" - where - "exif"."assetId" = "assets"."id" - ) as "exifInfo" on true - where - "assets"."deletedAt" is null - and "assets"."stackId" = "asset_stack"."id" - ) as agg - ) as "assets" -from - "asset_stack" +delete from "asset_stack" where "id" = $1::uuid diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 80491be973..3211527531 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -12,6 +12,7 @@ export interface AlbumAssetCount { assetCount: number; startDate: Date | null; endDate: Date | null; + lastModifiedAssetTimestamp: Date | null; } export interface AlbumInfoOptions { @@ -132,18 +133,21 @@ export class AlbumRepository { return []; } - return this.db - .selectFrom('albums') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') - .innerJoin('assets', 'assets.id', 'album_assets.assetsId') - .select('albums.id as albumId') - .select((eb) => eb.fn.min('assets.localDateTime').as('startDate')) - .select((eb) => eb.fn.max('assets.localDateTime').as('endDate')) - .select((eb) => sql`${eb.fn.count('assets.id')}::int`.as('assetCount')) - .where('albums.id', 'in', ids) - .where('assets.deletedAt', 'is', null) - .groupBy('albums.id') - .execute(); + return ( + this.db + .selectFrom('assets') + .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') + .select('album_assets.albumsId as albumId') + .select((eb) => eb.fn.min(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) + .select((eb) => eb.fn.max(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('endDate')) + // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need + .select((eb) => eb.fn.max('assets.updatedAt').as('lastModifiedAssetTimestamp')) + .select((eb) => sql`${eb.fn.count('assets.id')}::int`.as('assetCount')) + .where('album_assets.albumsId', 'in', ids) + .where('assets.deletedAt', 'is', null) + .groupBy('album_assets.albumsId') + .execute() + ); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 896110d39b..77154bbd1d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, UpdateResult, Updateable, sql } from 'kysely'; +import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db'; @@ -728,17 +728,6 @@ export class AssetRepository { return paginationHelper(items as any as AssetEntity[], pagination.take); } - getLastUpdatedAssetForAlbumId(albumId: string): Promise { - return this.db - .selectFrom('assets') - .selectAll('assets') - .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId') - .where('albums_assets_assets.albumsId', '=', asUuid(albumId)) - .orderBy('updatedAt', 'desc') - .limit(1) - .executeTakeFirst() as Promise; - } - getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise { return this.db .selectFrom('assets') @@ -978,6 +967,9 @@ export class AssetRepository { .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) .where('assets.ownerId', '=', asUuid(ownerId)) .where('assets.isVisible', '=', true) + .where('assets.fileCreatedAt', 'is not', null) + .where('assets.fileModifiedAt', 'is not', null) + .where('assets.localDateTime', 'is not', null) .where('assets.updatedAt', '<=', updatedUntil) .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!)) .orderBy('assets.id') @@ -1006,6 +998,9 @@ export class AssetRepository { .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) .where('assets.ownerId', '=', anyUuid(options.userIds)) .where('assets.isVisible', '=', true) + .where('assets.fileCreatedAt', 'is not', null) + .where('assets.fileModifiedAt', 'is not', null) + .where('assets.localDateTime', 'is not', null) .where('assets.updatedAt', '>', options.updatedAfter) .limit(options.limit) .execute() as any as Promise; @@ -1041,6 +1036,17 @@ export class AssetRepository { .execute(); } + async deleteFiles(files: Pick, 'id'>[]): Promise { + if (files.length === 0) { + return; + } + + await this.db + .deleteFrom('asset_files') + .where('id', '=', anyUuid(files.map((file) => file.id))) + .execute(); + } + @GenerateSql({ params: [{ libraryId: DummyValue.UUID, importPaths: [DummyValue.STRING], exclusionPatterns: [DummyValue.STRING] }], }) @@ -1064,7 +1070,7 @@ export class AssetRepository { .where((eb) => eb.or([ eb.not(eb.or(paths.map((path) => eb('originalPath', 'like', path)))), - eb('originalPath', 'like', exclusions.join('|')), + eb.or(exclusions.map((path) => eb('originalPath', 'like', path))), ]), ) .executeTakeFirstOrThrow(); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 2d5f2bc2e2..8cbb87b0c5 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -316,9 +316,9 @@ const getEnv = (): EnvData => { config: { typeorm: { type: 'postgres', - entities: [`${folders.dist}/entities` + '/*.entity.{js,ts}'], + entities: [], migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'], - subscribers: [`${folders.dist}/subscribers` + '/*.{js,ts}'], + subscribers: [], migrationsRun: false, synchronize: false, connectTimeoutMS: 10_000, // 10 seconds diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index c4aeb74028..43d3d2c16c 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,7 +1,10 @@ import { Injectable } from '@nestjs/common'; import AsyncLock from 'async-lock'; -import { Kysely, sql, Transaction } from 'kysely'; +import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; +import { existsSync } from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; import semver from 'semver'; import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { DB } from 'src/db'; @@ -200,9 +203,52 @@ export class DatabaseRepository { this.logger.log('Running migrations, this may take a while'); + this.logger.debug('Running typeorm migrations'); + await dataSource.initialize(); await dataSource.runMigrations(options); await dataSource.destroy(); + + this.logger.debug('Finished running typeorm migrations'); + + // eslint-disable-next-line unicorn/prefer-module + const migrationFolder = join(__dirname, '..', 'schema/migrations'); + + // TODO remove after we have at least one kysely migration + if (!existsSync(migrationFolder)) { + return; + } + + this.logger.debug('Running kysely migrations'); + const migrator = new Migrator({ + db: this.db, + migrationLockTableName: 'kysely_migrations_lock', + migrationTableName: 'kysely_migrations', + provider: new FileMigrationProvider({ + fs: { readdir }, + path: { join }, + migrationFolder, + }), + }); + + const { error, results } = await migrator.migrateToLatest(); + + for (const result of results ?? []) { + if (result.status === 'Success') { + this.logger.log(`Migration "${result.migrationName}" succeeded`); + } + + if (result.status === 'Error') { + this.logger.warn(`Migration "${result.migrationName}" failed`); + } + } + + if (error) { + this.logger.error(`Kysely migrations failed: ${error}`); + throw error; + } + + this.logger.debug('Finished running kysely migrations'); } async withLock(lock: DatabaseLock, callback: () => Promise): Promise { diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 5e916c71f3..a52bc58bc3 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -53,6 +53,7 @@ export interface Face { export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse; export type DetectedFaces = { faces: Face[] } & VisualResponse; export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest; +export type TextEncodingOptions = ModelOptions & { language?: string }; @Injectable() export class MachineLearningRepository { @@ -91,7 +92,9 @@ export class MachineLearningRepository { signal: AbortSignal.timeout(MACHINE_LEARNING_PING_TIMEOUT), }); active = response.ok; - } catch {} + } catch { + // nothing to do here + } this.setUrlAvailability(url, active); return active; } @@ -168,8 +171,8 @@ export class MachineLearningRepository { return response[ModelTask.SEARCH]; } - async encodeText(urls: string[], text: string, { modelName }: CLIPConfig) { - const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName } } }; + async encodeText(urls: string[], text: string, { language, modelName }: TextEncodingOptions) { + const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } }; const response = await this.predict(urls, { text }, request); return response[ModelTask.SEARCH]; } diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 483bd3fd90..d9cac0b018 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { exiftool } from 'exiftool-vendored'; +import { ExifDateTime, exiftool, WriteTags } from 'exiftool-vendored'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import { Duration } from 'luxon'; import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; import sharp from 'sharp'; import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants'; +import { ExifEntity } from 'src/entities/exif.entity'; import { Colorspace, LogLevel } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { @@ -43,20 +44,66 @@ export class MediaRepository { async extract(input: string, output: string): Promise { try { - await exiftool.extractJpgFromRaw(input, output); - } catch (error: any) { - this.logger.debug('Could not extract JPEG from image, trying preview', error.message); + // remove existing output file if it exists + // as exiftool-vendored does not support overwriting via "-w!" flag + // and throws "1 files could not be read" error when the output file exists + await fs.unlink(output).catch(() => null); + await exiftool.extractBinaryTag('JpgFromRaw2', input, output); + } catch { try { - await exiftool.extractPreview(input, output); + this.logger.debug('Extracting JPEG from RAW image:', input); + await exiftool.extractJpgFromRaw(input, output); } catch (error: any) { - this.logger.debug('Could not extract preview from image', error.message); - return false; + this.logger.debug('Could not extract JPEG from image, trying preview', error.message); + try { + await exiftool.extractPreview(input, output); + } catch (error: any) { + this.logger.debug('Could not extract preview from image', error.message); + return false; + } } } - return true; } + async writeExif(tags: Partial, output: string): Promise { + try { + const tagsToWrite: WriteTags = { + ExifImageWidth: tags.exifImageWidth, + ExifImageHeight: tags.exifImageHeight, + DateTimeOriginal: tags.dateTimeOriginal && ExifDateTime.fromMillis(tags.dateTimeOriginal.getTime()), + ModifyDate: tags.modifyDate && ExifDateTime.fromMillis(tags.modifyDate.getTime()), + TimeZone: tags.timeZone, + GPSLatitude: tags.latitude, + GPSLongitude: tags.longitude, + ProjectionType: tags.projectionType, + City: tags.city, + Country: tags.country, + Make: tags.make, + Model: tags.model, + LensModel: tags.lensModel, + Fnumber: tags.fNumber?.toFixed(1), + FocalLength: tags.focalLength?.toFixed(1), + ISO: tags.iso, + ExposureTime: tags.exposureTime, + ProfileDescription: tags.profileDescription, + ColorSpace: tags.colorspace, + Rating: tags.rating, + // specially convert Orientation to numeric Orientation# for exiftool + 'Orientation#': tags.orientation ? Number(tags.orientation) : undefined, + }; + + await exiftool.write(output, tagsToWrite, { + ignoreMinorErrors: true, + writeArgs: ['-overwrite_original'], + }); + return true; + } catch (error: any) { + this.logger.warn(`Could not write exif data to image: ${error.message}`); + return false; + } + } + decodeImage(input: string, options: DecodeToBufferOptions) { return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true }); } @@ -97,7 +144,10 @@ export class MediaRepository { pipeline = pipeline.extract(options.crop); } - return pipeline.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }); + if (options.size !== undefined) { + pipeline = pipeline.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }); + } + return pipeline; } async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise { diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index 6d17501d03..1d0770af6b 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -6,6 +6,7 @@ describe(NotificationRepository.name, () => { let sut: NotificationRepository; beforeEach(() => { + // eslint-disable-next-line no-sparse-arrays sut = new NotificationRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index d5855d3b91..751f97fdeb 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -9,7 +9,6 @@ import { PersonEntity } from 'src/entities/person.entity'; import { SourceType } from 'src/enum'; import { removeUndefinedKeys } from 'src/utils/database'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; -import { FindOptionsRelations } from 'typeorm'; export interface PersonSearchOptions { minimumFaceCount: number; @@ -85,7 +84,7 @@ export class PersonRepository { .$if(!!faceIds, (qb) => qb.where('asset_faces.id', 'in', faceIds!)) .executeTakeFirst(); - return Number(result.numChangedRows) ?? 0; + return Number(result.numChangedRows ?? 0); } @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] }) @@ -247,7 +246,7 @@ export class PersonRepository { @GenerateSql({ params: [DummyValue.UUID] }) getFaceByIdWithAssets( id: string, - relations?: FindOptionsRelations, + relations?: { faceSearch?: boolean }, select?: SelectFaceOptions, ): Promise { return this.db @@ -270,7 +269,7 @@ export class PersonRepository { .where('asset_faces.id', '=', assetFaceId) .executeTakeFirst(); - return Number(result.numChangedRows) ?? 0; + return Number(result.numChangedRows ?? 0); } getById(personId: string): Promise { diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index ae96005350..501067072d 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -122,38 +122,11 @@ export class StackRepository { @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - const stack = await this.getById(id); - if (!stack) { - return; - } - - const assetIds = stack.assets.map(({ id }) => id); - await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute(); - await this.db - .updateTable('assets') - .set({ stackId: null, updatedAt: new Date() }) - .where('id', 'in', assetIds) - .execute(); } async deleteAll(ids: string[]): Promise { - const assetIds = []; - for (const id of ids) { - const stack = await this.getById(id); - if (!stack) { - continue; - } - - assetIds.push(...stack.assets.map(({ id }) => id)); - } - - await this.db - .updateTable('assets') - .set({ updatedAt: new Date(), stackId: null }) - .where('id', 'in', assetIds) - .where('stackId', 'in', ids) - .execute(); + await this.db.deleteFrom('asset_stack').where('id', 'in', ids).execute(); } update(id: string, entity: Updateable): Promise { diff --git a/server/src/repositories/storage.repository.spec.ts b/server/src/repositories/storage.repository.spec.ts index 85ff4a746f..aaf875d3b3 100644 --- a/server/src/repositories/storage.repository.spec.ts +++ b/server/src/repositories/storage.repository.spec.ts @@ -183,6 +183,7 @@ describe(StorageRepository.name, () => { let sut: StorageRepository; beforeEach(() => { + // eslint-disable-next-line no-sparse-arrays sut = new StorageRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts index a110b9bc44..2038f204f7 100644 --- a/server/src/repositories/system-metadata.repository.ts +++ b/server/src/repositories/system-metadata.repository.ts @@ -4,7 +4,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { readFile } from 'node:fs/promises'; import { DB, SystemMetadata as DbSystemMetadata } from 'src/db'; import { GenerateSql } from 'src/decorators'; -import { SystemMetadata } from 'src/entities/system-metadata.entity'; +import { SystemMetadata } from 'src/types'; type Upsert = Insertable; diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 055df9dfdc..c254085fd2 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -3,11 +3,12 @@ import { Insertable, Kysely, sql, Updateable } from 'kysely'; import { DateTime } from 'luxon'; import { InjectKysely } from 'nestjs-kysely'; import { columns, UserAdmin } from 'src/database'; -import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db'; +import { DB, UserMetadata as DbUserMetadata } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; import { UserEntity, withMetadata } from 'src/entities/user.entity'; import { AssetType, UserStatus } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; import { asUuid } from 'src/utils/database'; type Upsert = Insertable; @@ -128,7 +129,7 @@ export class UserRepository { .execute() as Promise; } - async create(dto: Insertable): Promise { + async create(dto: Insertable): Promise { return this.db .insertInto('users') .values(dto) @@ -136,7 +137,7 @@ export class UserRepository { .executeTakeFirst() as unknown as Promise; } - update(id: string, dto: Updateable): Promise { + update(id: string, dto: Updateable): Promise { return this.db .updateTable('users') .set(dto) @@ -235,7 +236,6 @@ export class UserRepository { stat.usage = Number(stat.usage); stat.usagePhotos = Number(stat.usagePhotos); stat.usageVideos = Number(stat.usageVideos); - stat.quotaSizeInBytes = stat.quotaSizeInBytes; } return stats; diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts new file mode 100644 index 0000000000..87597838c7 --- /dev/null +++ b/server/src/schema/tables/activity.table.ts @@ -0,0 +1,56 @@ +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Check, + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + Index, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('activity') +@Index({ + name: 'IDX_activity_like', + columns: ['assetId', 'userId', 'albumId'], + unique: true, + where: '("isLiked" = true)', +}) +@Check({ + name: 'CHK_2ab1e70f113f450eb40c1e3ec8', + expression: `("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)`, +}) +export class ActivityTable { + @PrimaryGeneratedColumn() + id!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_activity_update_id') + @UpdateIdColumn() + updateId!: string; + + @Column({ type: 'text', default: null }) + comment!: string | null; + + @Column({ type: 'boolean', default: false }) + isLiked!: boolean; + + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) + assetId!: string | null; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + userId!: string; + + @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + albumId!: string; +} diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts new file mode 100644 index 0000000000..ccd7fda5fd --- /dev/null +++ b/server/src/schema/tables/album-asset.table.ts @@ -0,0 +1,27 @@ +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' }) +export class AlbumAssetTable { + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + nullable: false, + primary: true, + }) + @ColumnIndex() + assetsId!: string; + + @ForeignKeyColumn(() => AlbumTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + nullable: false, + primary: true, + }) + @ColumnIndex() + albumsId!: string; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts new file mode 100644 index 0000000000..8bd05df2ee --- /dev/null +++ b/server/src/schema/tables/album-user.table.ts @@ -0,0 +1,29 @@ +import { AlbumUserRole } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; + +@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' }) +// Pre-existing indices from original album <--> user ManyToMany mapping +@Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] }) +@Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] }) +export class AlbumUserTable { + @ForeignKeyColumn(() => AlbumTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + nullable: false, + primary: true, + }) + albumsId!: string; + + @ForeignKeyColumn(() => UserTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + nullable: false, + primary: true, + }) + usersId!: string; + + @Column({ type: 'character varying', default: AlbumUserRole.EDITOR }) + role!: AlbumUserRole; +} diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts new file mode 100644 index 0000000000..cf2f2e1cb4 --- /dev/null +++ b/server/src/schema/tables/album.table.ts @@ -0,0 +1,51 @@ +import { AssetOrder } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' }) +export class AlbumTable { + @PrimaryGeneratedColumn() + id!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + ownerId!: string; + + @Column({ default: 'Untitled Album' }) + albumName!: string; + + @Column({ type: 'text', default: '' }) + description!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_albums_update_id') + @UpdateIdColumn() + updateId?: string; + + @DeleteDateColumn() + deletedAt!: Date | null; + + @ForeignKeyColumn(() => AssetTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) + albumThumbnailAssetId!: string; + + @Column({ type: 'boolean', default: true }) + isActivityEnabled!: boolean; + + @Column({ default: AssetOrder.DESC }) + order!: AssetOrder; +} diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts new file mode 100644 index 0000000000..42b98ab957 --- /dev/null +++ b/server/src/schema/tables/api-key.table.ts @@ -0,0 +1,40 @@ +import { Permission } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('api_keys') +export class APIKeyTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column() + name!: string; + + @Column() + key!: string; + + @Column({ array: true, type: 'character varying' }) + permissions!: Permission[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex({ name: 'IDX_api_keys_update_id' }) + @UpdateIdColumn() + updateId?: string; + + @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + userId!: string; +} diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts new file mode 100644 index 0000000000..10f7b535bc --- /dev/null +++ b/server/src/schema/tables/asset-audit.table.ts @@ -0,0 +1,19 @@ +import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table('assets_audit') +export class AssetAuditTable { + @PrimaryGeneratedColumn({ type: 'v7' }) + id!: string; + + @ColumnIndex('IDX_assets_audit_asset_id') + @Column({ type: 'uuid' }) + assetId!: string; + + @ColumnIndex('IDX_assets_audit_owner_id') + @Column({ type: 'uuid' }) + ownerId!: string; + + @ColumnIndex('IDX_assets_audit_deleted_at') + @CreateDateColumn({ default: () => 'clock_timestamp()' }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts new file mode 100644 index 0000000000..56f22cf9a7 --- /dev/null +++ b/server/src/schema/tables/asset-face.table.ts @@ -0,0 +1,42 @@ +import { SourceType } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { PersonTable } from 'src/schema/tables/person.table'; +import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'asset_faces' }) +@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) +@Index({ columns: ['personId', 'assetId'] }) +export class AssetFaceTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column({ default: 0, type: 'integer' }) + imageWidth!: number; + + @Column({ default: 0, type: 'integer' }) + imageHeight!: number; + + @Column({ default: 0, type: 'integer' }) + boundingBoxX1!: number; + + @Column({ default: 0, type: 'integer' }) + boundingBoxY1!: number; + + @Column({ default: 0, type: 'integer' }) + boundingBoxX2!: number; + + @Column({ default: 0, type: 'integer' }) + boundingBoxY2!: number; + + @Column({ default: SourceType.MACHINE_LEARNING, enumName: 'sourcetype', enum: SourceType }) + sourceType!: SourceType; + + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + assetId!: string; + + @ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true }) + personId!: string | null; + + @DeleteDateColumn() + deletedAt!: Date | null; +} diff --git a/server/src/schema/tables/asset-files.table.ts b/server/src/schema/tables/asset-files.table.ts new file mode 100644 index 0000000000..fb32070751 --- /dev/null +++ b/server/src/schema/tables/asset-files.table.ts @@ -0,0 +1,40 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetFileType } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + Unique, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Unique({ name: 'UQ_assetId_type', columns: ['assetId', 'type'] }) +@Table('asset_files') +export class AssetFileTable { + @PrimaryGeneratedColumn() + id!: string; + + @ColumnIndex('IDX_asset_files_assetId') + @ForeignKeyColumn(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + assetId?: AssetEntity; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_asset_files_update_id') + @UpdateIdColumn() + updateId?: string; + + @Column() + type!: AssetFileType; + + @Column() + path!: string; +} diff --git a/server/src/schema/tables/asset-job-status.table.ts b/server/src/schema/tables/asset-job-status.table.ts new file mode 100644 index 0000000000..669ea0a20d --- /dev/null +++ b/server/src/schema/tables/asset-job-status.table.ts @@ -0,0 +1,23 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { Column, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table('asset_job_status') +export class AssetJobStatusTable { + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', primary: true }) + assetId!: string; + + @Column({ type: 'timestamp with time zone', nullable: true }) + facesRecognizedAt!: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + metadataExtractedAt!: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + duplicatesDetectedAt!: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + previewAt!: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + thumbnailAt!: Date | null; +} diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts new file mode 100644 index 0000000000..bd79d48149 --- /dev/null +++ b/server/src/schema/tables/asset.table.ts @@ -0,0 +1,138 @@ +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; +import { AssetStatus, AssetType } from 'src/enum'; +import { LibraryTable } from 'src/schema/tables/library.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + Index, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('assets') +// Checksums must be unique per user and library +@Index({ + name: ASSET_CHECKSUM_CONSTRAINT, + columns: ['ownerId', 'checksum'], + unique: true, + where: '("libraryId" IS NULL)', +}) +@Index({ + name: 'UQ_assets_owner_library_checksum' + '', + columns: ['ownerId', 'libraryId', 'checksum'], + unique: true, + where: '("libraryId" IS NOT NULL)', +}) +@Index({ name: 'idx_local_date_time', expression: `(("localDateTime" AT TIME ZONE 'UTC'::text))::date` }) +@Index({ + name: 'idx_local_date_time_month', + expression: `(date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text)`, +}) +@Index({ name: 'IDX_originalPath_libraryId', columns: ['originalPath', 'libraryId'] }) +@Index({ name: 'IDX_asset_id_stackId', columns: ['id', 'stackId'] }) +@Index({ + name: 'idx_originalFileName_trigram', + using: 'gin', + expression: 'f_unaccent(("originalFileName")::text)', +}) +// For all assets, each originalpath must be unique per user and library +export class AssetTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column() + deviceAssetId!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + ownerId!: string; + + @ForeignKeyColumn(() => LibraryTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) + libraryId?: string | null; + + @Column() + deviceId!: string; + + @Column() + type!: AssetType; + + @Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE }) + status!: AssetStatus; + + @Column() + originalPath!: string; + + @Column({ type: 'bytea', nullable: true }) + thumbhash!: Buffer | null; + + @Column({ type: 'character varying', nullable: true, default: '' }) + encodedVideoPath!: string | null; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_assets_update_id') + @UpdateIdColumn() + updateId?: string; + + @DeleteDateColumn() + deletedAt!: Date | null; + + @ColumnIndex('idx_asset_file_created_at') + @Column({ type: 'timestamp with time zone', default: null }) + fileCreatedAt!: Date; + + @Column({ type: 'timestamp with time zone', default: null }) + localDateTime!: Date; + + @Column({ type: 'timestamp with time zone', default: null }) + fileModifiedAt!: Date; + + @Column({ type: 'boolean', default: false }) + isFavorite!: boolean; + + @Column({ type: 'boolean', default: false }) + isArchived!: boolean; + + @Column({ type: 'boolean', default: false }) + isExternal!: boolean; + + @Column({ type: 'boolean', default: false }) + isOffline!: boolean; + + @Column({ type: 'bytea' }) + @ColumnIndex() + checksum!: Buffer; // sha1 checksum + + @Column({ type: 'character varying', nullable: true }) + duration!: string | null; + + @Column({ type: 'boolean', default: true }) + isVisible!: boolean; + + @ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' }) + livePhotoVideoId!: string | null; + + @Column() + @ColumnIndex() + originalFileName!: string; + + @Column({ nullable: true }) + sidecarPath!: string | null; + + @ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) + stackId?: string | null; + + @ColumnIndex('IDX_assets_duplicateId') + @Column({ type: 'uuid', nullable: true }) + duplicateId!: string | null; +} diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts new file mode 100644 index 0000000000..a05b070ba7 --- /dev/null +++ b/server/src/schema/tables/audit.table.ts @@ -0,0 +1,24 @@ +import { DatabaseAction, EntityType } from 'src/enum'; +import { Column, CreateDateColumn, Index, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table('audit') +@Index({ name: 'IDX_ownerId_createdAt', columns: ['ownerId', 'createdAt'] }) +export class AuditTable { + @PrimaryColumn({ type: 'integer', default: 'increment', synchronize: false }) + id!: number; + + @Column() + entityType!: EntityType; + + @Column({ type: 'uuid' }) + entityId!: string; + + @Column() + action!: DatabaseAction; + + @Column({ type: 'uuid' }) + ownerId!: string; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/server/src/schema/tables/exif.table.ts b/server/src/schema/tables/exif.table.ts new file mode 100644 index 0000000000..8eddafecc2 --- /dev/null +++ b/server/src/schema/tables/exif.table.ts @@ -0,0 +1,105 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools'; + +@Table('exif') +export class ExifTable { + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true }) + assetId!: string; + + @UpdateDateColumn({ default: () => 'clock_timestamp()' }) + updatedAt?: Date; + + @ColumnIndex('IDX_asset_exif_update_id') + @UpdateIdColumn() + updateId?: string; + + /* General info */ + @Column({ type: 'text', default: '' }) + description!: string; // or caption + + @Column({ type: 'integer', nullable: true }) + exifImageWidth!: number | null; + + @Column({ type: 'integer', nullable: true }) + exifImageHeight!: number | null; + + @Column({ type: 'bigint', nullable: true }) + fileSizeInByte!: number | null; + + @Column({ type: 'character varying', nullable: true }) + orientation!: string | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + dateTimeOriginal!: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + modifyDate!: Date | null; + + @Column({ type: 'character varying', nullable: true }) + timeZone!: string | null; + + @Column({ type: 'double precision', nullable: true }) + latitude!: number | null; + + @Column({ type: 'double precision', nullable: true }) + longitude!: number | null; + + @Column({ type: 'character varying', nullable: true }) + projectionType!: string | null; + + @ColumnIndex('exif_city') + @Column({ type: 'character varying', nullable: true }) + city!: string | null; + + @ColumnIndex('IDX_live_photo_cid') + @Column({ type: 'character varying', nullable: true }) + livePhotoCID!: string | null; + + @ColumnIndex('IDX_auto_stack_id') + @Column({ type: 'character varying', nullable: true }) + autoStackId!: string | null; + + @Column({ type: 'character varying', nullable: true }) + state!: string | null; + + @Column({ type: 'character varying', nullable: true }) + country!: string | null; + + /* Image info */ + @Column({ type: 'character varying', nullable: true }) + make!: string | null; + + @Column({ type: 'character varying', nullable: true }) + model!: string | null; + + @Column({ type: 'character varying', nullable: true }) + lensModel!: string | null; + + @Column({ type: 'double precision', nullable: true }) + fNumber!: number | null; + + @Column({ type: 'double precision', nullable: true }) + focalLength!: number | null; + + @Column({ type: 'integer', nullable: true }) + iso!: number | null; + + @Column({ type: 'character varying', nullable: true }) + exposureTime!: string | null; + + @Column({ type: 'character varying', nullable: true }) + profileDescription!: string | null; + + @Column({ type: 'character varying', nullable: true }) + colorspace!: string | null; + + @Column({ type: 'integer', nullable: true }) + bitsPerSample!: number | null; + + @Column({ type: 'integer', nullable: true }) + rating!: number | null; + + /* Video info */ + @Column({ type: 'double precision', nullable: true }) + fps?: number | null; +} diff --git a/server/src/schema/tables/face-search.table.ts b/server/src/schema/tables/face-search.table.ts new file mode 100644 index 0000000000..d4da6a69ba --- /dev/null +++ b/server/src/schema/tables/face-search.table.ts @@ -0,0 +1,16 @@ +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' }) +export class FaceSearchTable { + @ForeignKeyColumn(() => AssetFaceTable, { + onDelete: 'CASCADE', + primary: true, + constraintName: 'face_search_faceId_fkey', + }) + faceId!: string; + + @ColumnIndex({ name: 'face_index', synchronize: false }) + @Column({ type: 'vector', array: true, length: 512, synchronize: false }) + embedding!: string; +} diff --git a/server/src/schema/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts new file mode 100644 index 0000000000..5216a295cb --- /dev/null +++ b/server/src/schema/tables/geodata-places.table.ts @@ -0,0 +1,73 @@ +import { Column, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'geodata_places', synchronize: false }) +export class GeodataPlacesTable { + @PrimaryColumn({ type: 'integer' }) + id!: number; + + @Column({ type: 'character varying', length: 200 }) + name!: string; + + @Column({ type: 'double precision' }) + longitude!: number; + + @Column({ type: 'double precision' }) + latitude!: number; + + @Column({ type: 'character', length: 2 }) + countryCode!: string; + + @Column({ type: 'character varying', length: 20, nullable: true }) + admin1Code!: string; + + @Column({ type: 'character varying', length: 80, nullable: true }) + admin2Code!: string; + + @Column({ type: 'character varying', nullable: true }) + admin1Name!: string; + + @Column({ type: 'character varying', nullable: true }) + admin2Name!: string; + + @Column({ type: 'character varying', nullable: true }) + alternateNames!: string; + + @Column({ type: 'date' }) + modificationDate!: Date; +} + +@Table({ name: 'geodata_places_tmp', synchronize: false }) +export class GeodataPlacesTempEntity { + @PrimaryColumn({ type: 'integer' }) + id!: number; + + @Column({ type: 'character varying', length: 200 }) + name!: string; + + @Column({ type: 'double precision' }) + longitude!: number; + + @Column({ type: 'double precision' }) + latitude!: number; + + @Column({ type: 'character', length: 2 }) + countryCode!: string; + + @Column({ type: 'character varying', length: 20, nullable: true }) + admin1Code!: string; + + @Column({ type: 'character varying', length: 80, nullable: true }) + admin2Code!: string; + + @Column({ type: 'character varying', nullable: true }) + admin1Name!: string; + + @Column({ type: 'character varying', nullable: true }) + admin2Name!: string; + + @Column({ type: 'character varying', nullable: true }) + alternateNames!: string; + + @Column({ type: 'date' }) + modificationDate!: Date; +} diff --git a/server/src/schema/tables/index.ts b/server/src/schema/tables/index.ts new file mode 100644 index 0000000000..6991d957ae --- /dev/null +++ b/server/src/schema/tables/index.ts @@ -0,0 +1,73 @@ +import { ActivityTable } from 'src/schema/tables/activity.table'; +import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; +import { AlbumUserTable } from 'src/schema/tables/album-user.table'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { APIKeyTable } from 'src/schema/tables/api-key.table'; +import { AssetAuditTable } from 'src/schema/tables/asset-audit.table'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { AuditTable } from 'src/schema/tables/audit.table'; +import { ExifTable } from 'src/schema/tables/exif.table'; +import { FaceSearchTable } from 'src/schema/tables/face-search.table'; +import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; +import { LibraryTable } from 'src/schema/tables/library.table'; +import { MemoryTable } from 'src/schema/tables/memory.table'; +import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table'; +import { MoveTable } from 'src/schema/tables/move.table'; +import { + NaturalEarthCountriesTable, + NaturalEarthCountriesTempTable, +} from 'src/schema/tables/natural-earth-countries.table'; +import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table'; +import { PartnerTable } from 'src/schema/tables/partner.table'; +import { PersonTable } from 'src/schema/tables/person.table'; +import { SessionTable } from 'src/schema/tables/session.table'; +import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { SmartSearchTable } from 'src/schema/tables/smart-search.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table'; +import { SystemMetadataTable } from 'src/schema/tables/system-metadata.table'; +import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; +import { UserAuditTable } from 'src/schema/tables/user-audit.table'; +import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; + +export const tables = [ + ActivityTable, + AlbumAssetTable, + AlbumUserTable, + AlbumTable, + APIKeyTable, + AssetAuditTable, + AssetFaceTable, + AssetJobStatusTable, + AssetTable, + AuditTable, + ExifTable, + FaceSearchTable, + GeodataPlacesTable, + LibraryTable, + MemoryAssetTable, + MemoryTable, + MoveTable, + NaturalEarthCountriesTable, + NaturalEarthCountriesTempTable, + PartnerAuditTable, + PartnerTable, + PersonTable, + SessionTable, + SharedLinkAssetTable, + SharedLinkTable, + SmartSearchTable, + StackTable, + SessionSyncCheckpointTable, + SystemMetadataTable, + TagAssetTable, + UserAuditTable, + UserMetadataTable, + UserTable, + VersionHistoryTable, +]; diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts new file mode 100644 index 0000000000..ff0bfd64f7 --- /dev/null +++ b/server/src/schema/tables/library.table.ts @@ -0,0 +1,46 @@ +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('libraries') +export class LibraryTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column() + name!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + ownerId!: string; + + @Column({ type: 'text', array: true }) + importPaths!: string[]; + + @Column({ type: 'text', array: true }) + exclusionPatterns!: string[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_libraries_update_id') + @UpdateIdColumn() + updateId?: string; + + @DeleteDateColumn() + deletedAt?: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + refreshedAt!: Date | null; +} diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts new file mode 100644 index 0000000000..91a0412649 --- /dev/null +++ b/server/src/schema/tables/memory.table.ts @@ -0,0 +1,60 @@ +import { MemoryType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { MemoryData } from 'src/types'; + +@Table('memories') +export class MemoryTable { + @PrimaryGeneratedColumn() + id!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_memories_update_id') + @UpdateIdColumn() + updateId?: string; + + @DeleteDateColumn() + deletedAt?: Date; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + ownerId!: string; + + @Column() + type!: T; + + @Column({ type: 'jsonb' }) + data!: MemoryData[T]; + + /** unless set to true, will be automatically deleted in the future */ + @Column({ type: 'boolean', default: false }) + isSaved!: boolean; + + /** memories are sorted in ascending order by this value */ + @Column({ type: 'timestamp with time zone' }) + memoryAt!: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + showAt?: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + hideAt?: Date; + + /** when the user last viewed the memory */ + @Column({ type: 'timestamp with time zone', nullable: true }) + seenAt?: Date; +} diff --git a/server/src/schema/tables/memory_asset.table.ts b/server/src/schema/tables/memory_asset.table.ts new file mode 100644 index 0000000000..08cdcea442 --- /dev/null +++ b/server/src/schema/tables/memory_asset.table.ts @@ -0,0 +1,14 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { MemoryTable } from 'src/schema/tables/memory.table'; +import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table('memories_assets_assets') +export class MemoryAssetTable { + @ColumnIndex() + @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + assetsId!: string; + + @ColumnIndex() + @ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + memoriesId!: string; +} diff --git a/server/src/schema/tables/move.table.ts b/server/src/schema/tables/move.table.ts new file mode 100644 index 0000000000..cdc00efcaf --- /dev/null +++ b/server/src/schema/tables/move.table.ts @@ -0,0 +1,24 @@ +import { PathType } from 'src/enum'; +import { Column, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools'; + +@Table('move_history') +// path lock (per entity) +@Unique({ name: 'UQ_entityId_pathType', columns: ['entityId', 'pathType'] }) +// new path lock (global) +@Unique({ name: 'UQ_newPath', columns: ['newPath'] }) +export class MoveTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column({ type: 'uuid' }) + entityId!: string; + + @Column({ type: 'character varying' }) + pathType!: PathType; + + @Column({ type: 'character varying' }) + oldPath!: string; + + @Column({ type: 'character varying' }) + newPath!: string; +} diff --git a/server/src/schema/tables/natural-earth-countries.table.ts b/server/src/schema/tables/natural-earth-countries.table.ts new file mode 100644 index 0000000000..5ac5384afc --- /dev/null +++ b/server/src/schema/tables/natural-earth-countries.table.ts @@ -0,0 +1,37 @@ +import { Column, PrimaryColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'naturalearth_countries', synchronize: false }) +export class NaturalEarthCountriesTable { + @PrimaryColumn({ type: 'serial' }) + id!: number; + + @Column({ type: 'character varying', length: 50 }) + admin!: string; + + @Column({ type: 'character varying', length: 3 }) + admin_a3!: string; + + @Column({ type: 'character varying', length: 50 }) + type!: string; + + @Column({ type: 'polygon' }) + coordinates!: string; +} + +@Table({ name: 'naturalearth_countries_tmp', synchronize: false }) +export class NaturalEarthCountriesTempTable { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ type: 'character varying', length: 50 }) + admin!: string; + + @Column({ type: 'character varying', length: 3 }) + admin_a3!: string; + + @Column({ type: 'character varying', length: 50 }) + type!: string; + + @Column({ type: 'polygon' }) + coordinates!: string; +} diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts new file mode 100644 index 0000000000..77d9f976b1 --- /dev/null +++ b/server/src/schema/tables/partner-audit.table.ts @@ -0,0 +1,19 @@ +import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table('partners_audit') +export class PartnerAuditTable { + @PrimaryGeneratedColumn({ type: 'v7' }) + id!: string; + + @ColumnIndex('IDX_partners_audit_shared_by_id') + @Column({ type: 'uuid' }) + sharedById!: string; + + @ColumnIndex('IDX_partners_audit_shared_with_id') + @Column({ type: 'uuid' }) + sharedWithId!: string; + + @ColumnIndex('IDX_partners_audit_deleted_at') + @CreateDateColumn({ default: () => 'clock_timestamp()' }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts new file mode 100644 index 0000000000..6406b48277 --- /dev/null +++ b/server/src/schema/tables/partner.table.ts @@ -0,0 +1,32 @@ +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('partners') +export class PartnerTable { + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) + sharedById!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) + sharedWithId!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_partners_update_id') + @UpdateIdColumn() + updateId!: string; + + @Column({ type: 'boolean', default: false }) + inTimeline!: boolean; +} diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts new file mode 100644 index 0000000000..91a05d8d76 --- /dev/null +++ b/server/src/schema/tables/person.table.ts @@ -0,0 +1,54 @@ +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Check, + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('person') +@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` }) +export class PersonTable { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_person_update_id') + @UpdateIdColumn() + updateId!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + ownerId!: string; + + @Column({ default: '' }) + name!: string; + + @Column({ type: 'date', nullable: true }) + birthDate!: Date | string | null; + + @Column({ default: '' }) + thumbnailPath!: string; + + @ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true }) + faceAssetId!: string | null; + + @Column({ type: 'boolean', default: false }) + isHidden!: boolean; + + @Column({ type: 'boolean', default: false }) + isFavorite!: boolean; + + @Column({ type: 'character varying', nullable: true, default: null }) + color?: string | null; +} diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts new file mode 100644 index 0000000000..287f13de7f --- /dev/null +++ b/server/src/schema/tables/session.table.ts @@ -0,0 +1,40 @@ +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' }) +export class SessionTable { + @PrimaryGeneratedColumn() + id!: string; + + // TODO convert to byte[] + @Column() + token!: string; + + @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + userId!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_sessions_update_id') + @UpdateIdColumn() + updateId!: string; + + @Column({ default: '' }) + deviceType!: string; + + @Column({ default: '' }) + deviceOS!: string; +} diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts new file mode 100644 index 0000000000..1eb294c1e8 --- /dev/null +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -0,0 +1,14 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table('shared_link__asset') +export class SharedLinkAssetTable { + @ColumnIndex() + @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + assetsId!: string; + + @ColumnIndex() + @ForeignKeyColumn(() => SharedLinkTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + sharedLinksId!: string; +} diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts new file mode 100644 index 0000000000..4372a5760a --- /dev/null +++ b/server/src/schema/tables/shared-link.table.ts @@ -0,0 +1,54 @@ +import { SharedLinkType } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + Unique, +} from 'src/sql-tools'; + +@Table('shared_links') +@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) +export class SharedLinkTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column({ type: 'character varying', nullable: true }) + description!: string | null; + + @Column({ type: 'character varying', nullable: true }) + password!: string | null; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + userId!: string; + + @ColumnIndex('IDX_sharedlink_albumId') + @ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + albumId!: string; + + @ColumnIndex('IDX_sharedlink_key') + @Column({ type: 'bytea' }) + key!: Buffer; // use to access the inidividual asset + + @Column() + type!: SharedLinkType; + + @CreateDateColumn() + createdAt!: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + expiresAt!: Date | null; + + @Column({ type: 'boolean', default: false }) + allowUpload!: boolean; + + @Column({ type: 'boolean', default: true }) + allowDownload!: boolean; + + @Column({ type: 'boolean', default: true }) + showExif!: boolean; +} diff --git a/server/src/schema/tables/smart-search.table.ts b/server/src/schema/tables/smart-search.table.ts new file mode 100644 index 0000000000..a71eb9ae99 --- /dev/null +++ b/server/src/schema/tables/smart-search.table.ts @@ -0,0 +1,16 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; + +@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' }) +export class SmartSearchTable { + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + primary: true, + constraintName: 'smart_search_assetId_fkey', + }) + assetId!: string; + + @ColumnIndex({ name: 'clip_index', synchronize: false }) + @Column({ type: 'vector', array: true, length: 512, synchronize: false }) + embedding!: string; +} diff --git a/server/src/schema/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts new file mode 100644 index 0000000000..ea58ccb425 --- /dev/null +++ b/server/src/schema/tables/stack.table.ts @@ -0,0 +1,16 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { ForeignKeyColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table('asset_stack') +export class StackTable { + @PrimaryGeneratedColumn() + id!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + ownerId!: string; + + //TODO: Add constraint to ensure primary asset exists in the assets array + @ForeignKeyColumn(() => AssetTable, { nullable: false, unique: true }) + primaryAssetId!: string; +} diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts new file mode 100644 index 0000000000..190cd81ffe --- /dev/null +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -0,0 +1,34 @@ +import { SyncEntityType } from 'src/enum'; +import { SessionTable } from 'src/schema/tables/session.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('session_sync_checkpoints') +export class SessionSyncCheckpointTable { + @ForeignKeyColumn(() => SessionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', primary: true }) + sessionId!: string; + + @PrimaryColumn({ type: 'character varying' }) + type!: SyncEntityType; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_session_sync_checkpoints_update_id') + @UpdateIdColumn() + updateId!: string; + + @Column() + ack!: string; +} diff --git a/server/src/schema/tables/system-metadata.table.ts b/server/src/schema/tables/system-metadata.table.ts new file mode 100644 index 0000000000..8657768db6 --- /dev/null +++ b/server/src/schema/tables/system-metadata.table.ts @@ -0,0 +1,12 @@ +import { SystemMetadataKey } from 'src/enum'; +import { Column, PrimaryColumn, Table } from 'src/sql-tools'; +import { SystemMetadata } from 'src/types'; + +@Table('system_metadata') +export class SystemMetadataTable { + @PrimaryColumn({ type: 'character varying' }) + key!: T; + + @Column({ type: 'jsonb' }) + value!: SystemMetadata[T]; +} diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts new file mode 100644 index 0000000000..5f24799cec --- /dev/null +++ b/server/src/schema/tables/tag-asset.table.ts @@ -0,0 +1,15 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { TagTable } from 'src/schema/tables/tag.table'; +import { ColumnIndex, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; + +@Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] }) +@Table('tag_asset') +export class TagAssetTable { + @ColumnIndex() + @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + assetsId!: string; + + @ColumnIndex() + @ForeignKeyColumn(() => TagTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + tagsId!: string; +} diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts new file mode 100644 index 0000000000..079dd4dcc5 --- /dev/null +++ b/server/src/schema/tables/tag-closure.table.ts @@ -0,0 +1,15 @@ +import { TagTable } from 'src/schema/tables/tag.table'; +import { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table('tags_closure') +export class TagClosureTable { + @PrimaryColumn() + @ColumnIndex() + @ForeignKeyColumn(() => TagTable, { onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + id_ancestor!: string; + + @PrimaryColumn() + @ColumnIndex() + @ForeignKeyColumn(() => TagTable, { onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + id_descendant!: string; +} diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts new file mode 100644 index 0000000000..1c6b8cb205 --- /dev/null +++ b/server/src/schema/tables/tag.table.ts @@ -0,0 +1,41 @@ +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + Unique, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +@Table('tags') +@Unique({ columns: ['userId', 'value'] }) +export class TagTable { + @PrimaryGeneratedColumn() + id!: string; + + @Column() + value!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ColumnIndex('IDX_tags_update_id') + @UpdateIdColumn() + updateId!: string; + + @Column({ type: 'character varying', nullable: true, default: null }) + color!: string | null; + + @ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' }) + parentId?: string; + + @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + userId!: string; +} diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts new file mode 100644 index 0000000000..e3f117381c --- /dev/null +++ b/server/src/schema/tables/user-audit.table.ts @@ -0,0 +1,14 @@ +import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table('users_audit') +export class UserAuditTable { + @PrimaryGeneratedColumn({ type: 'v7' }) + id!: string; + + @Column({ type: 'uuid' }) + userId!: string; + + @ColumnIndex('IDX_users_audit_deleted_at') + @CreateDateColumn({ default: () => 'clock_timestamp()' }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts new file mode 100644 index 0000000000..e71b3bf9f9 --- /dev/null +++ b/server/src/schema/tables/user-metadata.table.ts @@ -0,0 +1,16 @@ +import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; +import { UserMetadataKey } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; +import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table('user_metadata') +export class UserMetadataTable implements UserMetadataItem { + @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + userId!: string; + + @PrimaryColumn({ type: 'character varying' }) + key!: T; + + @Column({ type: 'jsonb' }) + value!: UserMetadata[T]; +} diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts new file mode 100644 index 0000000000..5bd9cd94c6 --- /dev/null +++ b/server/src/schema/tables/user.table.ts @@ -0,0 +1,73 @@ +import { ColumnType } from 'kysely'; +import { UserStatus } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + Index, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; + +type Timestamp = ColumnType; +type Generated = + T extends ColumnType ? ColumnType : ColumnType; + +@Table('users') +@Index({ name: 'IDX_users_updated_at_asc_id_asc', columns: ['updatedAt', 'id'] }) +export class UserTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @Column({ default: '' }) + name!: Generated; + + @Column({ type: 'boolean', default: false }) + isAdmin!: Generated; + + @Column({ unique: true }) + email!: string; + + @Column({ unique: true, nullable: true, default: null }) + storageLabel!: string | null; + + @Column({ default: '' }) + password!: Generated; + + @Column({ default: '' }) + oauthId!: Generated; + + @Column({ default: '' }) + profileImagePath!: Generated; + + @Column({ type: 'boolean', default: true }) + shouldChangePassword!: Generated; + + @CreateDateColumn() + createdAt!: Generated; + + @UpdateDateColumn() + updatedAt!: Generated; + + @DeleteDateColumn() + deletedAt!: Timestamp | null; + + @Column({ type: 'character varying', default: UserStatus.ACTIVE }) + status!: Generated; + + @ColumnIndex({ name: 'IDX_users_update_id' }) + @UpdateIdColumn() + updateId!: Generated; + + @Column({ type: 'bigint', nullable: true }) + quotaSizeInBytes!: ColumnType | null; + + @Column({ type: 'bigint', default: 0 }) + quotaUsageInBytes!: Generated>; + + @Column({ type: 'timestamp with time zone', default: () => 'now()' }) + profileChangedAt!: Generated; +} diff --git a/server/src/schema/tables/version-history.table.ts b/server/src/schema/tables/version-history.table.ts new file mode 100644 index 0000000000..18805a2de3 --- /dev/null +++ b/server/src/schema/tables/version-history.table.ts @@ -0,0 +1,13 @@ +import { Column, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; + +@Table('version_history') +export class VersionHistoryTable { + @PrimaryGeneratedColumn() + id!: string; + + @CreateDateColumn() + createdAt!: Date; + + @Column() + version!: string; +} diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 62d1326cab..a0fbfc0817 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -41,8 +41,20 @@ describe(AlbumService.name, () => { it('gets list of albums for auth user', async () => { mocks.album.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); mocks.album.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null }, - { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null }, + { + albumId: albumStub.empty.id, + assetCount: 0, + startDate: null, + endDate: null, + lastModifiedAssetTimestamp: null, + }, + { + albumId: albumStub.sharedWithUser.id, + assetCount: 0, + startDate: null, + endDate: null, + lastModifiedAssetTimestamp: null, + }, ]); const result = await sut.getAll(authStub.admin, {}); @@ -59,6 +71,7 @@ describe(AlbumService.name, () => { assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), + lastModifiedAssetTimestamp: new Date('1970-01-01'), }, ]); @@ -71,7 +84,13 @@ describe(AlbumService.name, () => { it('gets list of albums that are shared', async () => { mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]); mocks.album.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null }, + { + albumId: albumStub.sharedWithUser.id, + assetCount: 0, + startDate: null, + endDate: null, + lastModifiedAssetTimestamp: null, + }, ]); const result = await sut.getAll(authStub.admin, { shared: true }); @@ -83,7 +102,13 @@ describe(AlbumService.name, () => { it('gets list of albums that are NOT shared', async () => { mocks.album.getNotShared.mockResolvedValue([albumStub.empty]); mocks.album.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null }, + { + albumId: albumStub.empty.id, + assetCount: 0, + startDate: null, + endDate: null, + lastModifiedAssetTimestamp: null, + }, ]); const result = await sut.getAll(authStub.admin, { shared: false }); @@ -101,6 +126,7 @@ describe(AlbumService.name, () => { assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), + lastModifiedAssetTimestamp: new Date('1970-01-01'), }, ]); @@ -447,6 +473,7 @@ describe(AlbumService.name, () => { assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), + lastModifiedAssetTimestamp: new Date('1970-01-01'), }, ]); @@ -468,6 +495,7 @@ describe(AlbumService.name, () => { assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), + lastModifiedAssetTimestamp: new Date('1970-01-01'), }, ]); @@ -489,6 +517,7 @@ describe(AlbumService.name, () => { assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), + lastModifiedAssetTimestamp: new Date('1970-01-01'), }, ]); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 722745ebd2..994912f2c7 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -58,19 +58,15 @@ export class AlbumService extends BaseService { albumMetadata[metadata.albumId] = metadata; } - return Promise.all( - albums.map(async (album) => { - const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id); - return { - ...mapAlbumWithoutAssets(album), - sharedLinks: undefined, - startDate: albumMetadata[album.id]?.startDate ?? undefined, - endDate: albumMetadata[album.id]?.endDate ?? undefined, - assetCount: albumMetadata[album.id]?.assetCount ?? 0, - lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, - }; - }), - ); + return albums.map((album) => ({ + ...mapAlbumWithoutAssets(album), + sharedLinks: undefined, + startDate: albumMetadata[album.id]?.startDate ?? undefined, + endDate: albumMetadata[album.id]?.endDate ?? undefined, + assetCount: albumMetadata[album.id]?.assetCount ?? 0, + // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need + lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined, + })); } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { @@ -79,14 +75,13 @@ export class AlbumService extends BaseService { const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); - const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id); return { ...mapAlbum(album, withAssets, auth), startDate: albumMetadataForIds?.startDate ?? undefined, endDate: albumMetadataForIds?.endDate ?? undefined, assetCount: albumMetadataForIds?.assetCount ?? 0, - lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, + lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined, }; } diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index 71fb36b4f2..6bfbf307f6 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -103,7 +103,9 @@ export class ApiService { break; } } - } catch {} + } catch { + // nothing to do here + } res.type('text/html').header('Cache-Control', 'no-store').send(html); }; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 97736b905c..9499e788f4 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -584,7 +584,7 @@ describe(AssetMediaService.name, () => { sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), ).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.image.files[0].path, + path: '/uploads/user-id/thumbs/path.jpg', cacheControl: CacheControl.PRIVATE_WITH_CACHE, contentType: 'image/jpeg', fileName: 'asset-id_preview.jpg', @@ -599,7 +599,7 @@ describe(AssetMediaService.name, () => { sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), ).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.image.files[1].path, + path: '/uploads/user-id/webp/path.ext', cacheControl: CacheControl.PRIVATE_WITH_CACHE, contentType: 'application/octet-stream', fileName: 'asset-id_thumbnail.ext', diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 09ebd9db71..747d7e4514 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -37,6 +37,10 @@ interface UploadRequest { file: UploadFile; } +export interface AssetMediaRedirectResponse { + targetSize: AssetMediaSize | 'original'; +} + @Injectable() export class AssetMediaService extends BaseService { async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise { @@ -194,16 +198,31 @@ export class AssetMediaService extends BaseService { }); } - async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise { + async viewThumbnail( + auth: AuthDto, + id: string, + dto: AssetMediaOptionsDto, + ): Promise { await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; - const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files); let filepath = previewFile?.path; if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { filepath = thumbnailFile.path; + } else if (size === AssetMediaSize.FULLSIZE) { + if (mimeTypes.isWebSupportedImage(asset.originalPath)) { + // use original file for web supported images + return { targetSize: 'original' }; + } + if (!fullsizeFile) { + // downgrade to preview if fullsize is not available. + // e.g. disabled or not yet (re)generated + return { targetSize: AssetMediaSize.PREVIEW }; + } + filepath = fullsizeFile.path; } if (!filepath) { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index b977dd6d70..470f29fb3d 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -578,6 +578,7 @@ describe(AssetService.name, () => { files: [ '/uploads/user-id/webp/path.ext', '/uploads/user-id/thumbs/path.jpg', + '/uploads/user-id/fullsize/path.webp', assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, @@ -637,7 +638,14 @@ describe(AssetService.name, () => { { name: JobName.DELETE_FILES, data: { - files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'], + files: [ + '/uploads/user-id/webp/path.ext', + '/uploads/user-id/thumbs/path.jpg', + '/uploads/user-id/fullsize/path.webp', + undefined, + undefined, + 'fake_path/asset_1.jpeg', + ], }, }, ], @@ -658,7 +666,14 @@ describe(AssetService.name, () => { { name: JobName.DELETE_FILES, data: { - files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'], + files: [ + '/uploads/user-id/webp/path.ext', + '/uploads/user-id/thumbs/path.jpg', + '/uploads/user-id/fullsize/path.webp', + undefined, + undefined, + 'fake_path/asset_1.jpeg', + ], }, }, ], diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 37ec00b0bb..d05bb023f2 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -233,8 +233,8 @@ export class AssetService extends BaseService { } } - const { thumbnailFile, previewFile } = getAssetFiles(asset.files); - const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath]; + const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files); + const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { files.push(asset.sidecarPath, asset.originalPath); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 3948469765..a049a9c64b 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -136,8 +136,14 @@ export class AuditService extends BaseService { for await (const assets of pagination) { assetCount += assets.length; for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { - const { previewFile, thumbnailFile } = getAssetFiles(files); - for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) { + const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(files); + for (const file of [ + originalPath, + fullsizeFile?.path, + previewFile?.path, + encodedVideoPath, + thumbnailFile?.path, + ]) { track(file); } diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index e4fe791b19..dc4f71b992 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { default as path } from 'node:path'; +import path from 'node:path'; import semver from 'semver'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index f8c995c007..6739678561 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -4,7 +4,6 @@ import sanitize from 'sanitize-filename'; import { SystemConfig } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; @@ -47,6 +46,7 @@ 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 { UserTable } from 'src/schema/tables/user.table'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -138,7 +138,7 @@ export class BaseService { return checkAccess(this.accessRepository, request); } - async createUser(dto: Insertable & { email: string }): Promise { + async createUser(dto: Insertable & { email: string }): Promise { const user = await this.userRepository.getByEmail(dto.email); if (user) { throw new BadRequestException('User exists'); @@ -151,7 +151,7 @@ export class BaseService { } } - const payload: Insertable = { ...dto }; + const payload: Insertable = { ...dto }; if (payload.password) { payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); } diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 74b86f8e4e..da6c6794fb 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -5,12 +5,12 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; import { WithoutProperty } from 'src/repositories/asset.repository'; import { AssetDuplicateResult } from 'src/repositories/search.repository'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFile } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -69,7 +69,7 @@ export class DuplicateService extends BaseService { return JobStatus.SKIPPED; } - const { previewFile } = getAssetFiles(asset.files); + const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); if (!previewFile) { this.logger.warn(`Asset ${id} is missing preview image`); return JobStatus.FAILED; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 9b8cc820cc..a754fc47d0 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,5 +1,6 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; +import { AssetMediaSize } from 'src/dtos/asset-media.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { @@ -233,18 +234,44 @@ describe(MediaService.name, () => { }); await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.move.create).toHaveBeenCalledTimes(2); + expect(mocks.move.create).toHaveBeenCalledWith({ + entityId: assetStub.image.id, + pathType: AssetPathType.FULLSIZE, + oldPath: '/uploads/user-id/fullsize/path.webp', + newPath: 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + }); + expect(mocks.move.create).toHaveBeenCalledWith({ + entityId: assetStub.image.id, + pathType: AssetPathType.PREVIEW, + oldPath: '/uploads/user-id/thumbs/path.jpg', + newPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + }); + expect(mocks.move.create).toHaveBeenCalledWith({ + entityId: assetStub.image.id, + pathType: AssetPathType.THUMBNAIL, + oldPath: '/uploads/user-id/webp/path.ext', + newPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + }); + expect(mocks.move.create).toHaveBeenCalledTimes(3); }); }); describe('handleGenerateThumbnails', () => { let rawBuffer: Buffer; + let fullsizeBuffer: Buffer; let rawInfo: RawImageInfo; beforeEach(() => { + fullsizeBuffer = Buffer.from('embedded image data'); rawBuffer = Buffer.from('image data'); rawInfo = { width: 100, height: 100, channels: 3 }; - mocks.media.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo as OutputInfo }); + mocks.media.decodeImage.mockImplementation((path) => + Promise.resolve( + path.includes(AssetMediaSize.FULLSIZE) + ? { data: fullsizeBuffer, info: rawInfo as OutputInfo } + : { data: rawBuffer, info: rawInfo as OutputInfo }, + ), + ); }); it('should skip thumbnail generation if asset not found', async () => { @@ -591,15 +618,13 @@ describe(MediaService.name, () => { await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - const extractedPath = mocks.media.extract.mock.calls.at(-1)?.[1].toString(); + const convertedPath = mocks.media.extract.mock.lastCall?.[1].toString(); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(convertedPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, }); - expect(extractedPath?.endsWith('.tmp')).toBe(true); - expect(mocks.storage.unlink).toHaveBeenCalledWith(extractedPath); }); it('should resize original image if embedded image is too small', async () => { @@ -610,15 +635,14 @@ describe(MediaService.name, () => { await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + const extractedPath = mocks.media.extract.mock.lastCall?.[1].toString(); + expect(extractedPath).toMatch(/-fullsize\.jpeg$/); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, }); - const extractedPath = mocks.media.extract.mock.calls.at(-1)?.[1].toString(); - expect(extractedPath?.endsWith('.tmp')).toBe(true); - expect(mocks.storage.unlink).toHaveBeenCalledWith(extractedPath); }); it('should resize original image if embedded image not found', async () => { @@ -686,6 +710,159 @@ describe(MediaService.name, () => { expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); vi.unstubAllEnvs(); }); + + it('should generate full-size preview using embedded JPEG from RAW images when extractEmbedded is true', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: true } }); + mocks.media.extract.mockResolvedValue(true); + mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.asset.getById.mockResolvedValue(assetStub.imageDng); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + const extractedPath = mocks.media.extract.mock.lastCall?.[1].toString(); + expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + fullsizeBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + size: 1440, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + ); + }); + + it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } }); + mocks.media.extract.mockResolvedValue(true); + mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.asset.getById.mockResolvedValue(assetStub.imageDng); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + ); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + quality: 80, + size: 1440, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + ); + }); + + it('should generate full-size preview from non-web-friendly images', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); + mocks.media.extract.mockResolvedValue(true); + mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. + mocks.asset.getById.mockResolvedValue(assetStub.imageHif); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + ); + }); + + it('should skip generating full-size preview for web-friendly images', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); + mocks.media.extract.mockResolvedValue(true); + mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.asset.getById.mockResolvedValue(assetStub.image); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + colorspace: Colorspace.SRGB, + processInvalidImages: false, + size: 1440, + }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mocks.media.generateThumbnail).not.toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + ); + }); + + it('should respect encoding options when generating full-size preview', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + image: { fullsize: { enabled: true, format: ImageFormat.WEBP, quality: 90 } }, + }); + mocks.media.extract.mockResolvedValue(true); + mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. + mocks.asset.getById.mockResolvedValue(assetStub.imageHif); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.WEBP, + quality: 90, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-fullsize.webp', + ); + }); }); describe('handleQueueVideoConversion', () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 54540dff66..5318cdc97f 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { dirname } from 'node:path'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; @@ -11,6 +10,7 @@ import { AssetType, AudioCodec, Colorspace, + ImageFormat, JobName, JobStatus, LogLevel, @@ -24,7 +24,16 @@ import { } from 'src/enum'; import { UpsertFileOptions, WithoutProperty } from 'src/repositories/asset.repository'; import { BaseService } from 'src/services/base.service'; -import { AudioStreamInfo, JobItem, JobOf, VideoFormat, VideoInterfaces, VideoStreamInfo } from 'src/types'; +import { + AudioStreamInfo, + DecodeToBufferOptions, + GenerateThumbnailOptions, + JobItem, + JobOf, + VideoFormat, + VideoInterfaces, + VideoStreamInfo, +} from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; @@ -131,6 +140,7 @@ export class MediaService extends BaseService { return JobStatus.FAILED; } + await this.storageCore.moveAssetImage(asset, AssetPathType.FULLSIZE, image.fullsize.format); await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format); await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); await this.storageCore.moveAssetVideo(asset); @@ -151,7 +161,12 @@ export class MediaService extends BaseService { return JobStatus.SKIPPED; } - let generated: { previewPath: string; thumbnailPath: string; thumbhash: Buffer }; + let generated: { + previewPath: string; + thumbnailPath: string; + fullsizePath?: string; + thumbhash: Buffer; + }; if (asset.type === AssetType.VIDEO || asset.originalFileName.toLowerCase().endsWith('.gif')) { generated = await this.generateVideoThumbnails(asset); } else if (asset.type === AssetType.IMAGE) { @@ -161,7 +176,7 @@ export class MediaService extends BaseService { return JobStatus.SKIPPED; } - const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + const { previewFile, thumbnailFile, fullsizeFile } = getAssetFiles(asset.files); const toUpsert: UpsertFileOptions[] = []; if (previewFile?.path !== generated.previewPath) { toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.PREVIEW }); @@ -171,11 +186,15 @@ export class MediaService extends BaseService { toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.THUMBNAIL }); } + if (generated.fullsizePath && fullsizeFile?.path !== generated.fullsizePath) { + toUpsert.push({ assetId: asset.id, path: generated.fullsizePath, type: AssetFileType.FULLSIZE }); + } + if (toUpsert.length > 0) { await this.assetRepository.upsertFiles(toUpsert); } - const pathsToDelete = []; + const pathsToDelete: string[] = []; if (previewFile && previewFile.path !== generated.previewPath) { this.logger.debug(`Deleting old preview for asset ${asset.id}`); pathsToDelete.push(previewFile.path); @@ -186,6 +205,15 @@ export class MediaService extends BaseService { pathsToDelete.push(thumbnailFile.path); } + if (fullsizeFile && fullsizeFile.path !== generated.fullsizePath) { + this.logger.debug(`Deleting old fullsize preview image for asset ${asset.id}`); + pathsToDelete.push(fullsizeFile.path); + if (!generated.fullsizePath) { + // did not generate a new fullsize image, delete the existing record + await this.assetRepository.deleteFiles([fullsizeFile]); + } + } + if (pathsToDelete.length > 0) { await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path))); } @@ -205,33 +233,71 @@ export class MediaService extends BaseService { const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); - const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); - const extractedPath = StorageCore.getTempPathInDir(dirname(previewPath)); - const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); + const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true'; + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - try { - const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size)); - const inputPath = useExtracted ? extractedPath : asset.originalPath; - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true'; + // prevents this extra "enabled" from leaking into fullsizeOptions later + const { enabled: imageFullsizeEnabled, ...imageFullsizeConfig } = image.fullsize; - const orientation = useExtracted && asset.exifInfo?.orientation ? Number(asset.exifInfo.orientation) : undefined; - const decodeOptions = { colorspace, processInvalidImages, size: image.preview.size, orientation }; - const { data, info } = await this.mediaRepository.decodeImage(inputPath, decodeOptions); + const shouldConvertFullsize = imageFullsizeEnabled && !mimeTypes.isWebSupportedImage(asset.originalFileName); + const shouldExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName); + const decodeOptions: DecodeToBufferOptions = { colorspace, processInvalidImages, size: image.preview.size }; - const options = { colorspace, processInvalidImages, raw: info }; - const outputs = await Promise.all([ - this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...options }, thumbnailPath), - this.mediaRepository.generateThumbnail(data, { ...image.preview, ...options }, previewPath), - this.mediaRepository.generateThumbhash(data, options), - ]); + let useExtracted = false; + let decodeInputPath: string = asset.originalPath; + // Converted or extracted image from non-web-supported formats (e.g. RAW) + let fullsizePath: string | undefined; - return { previewPath, thumbnailPath, thumbhash: outputs[2] }; - } finally { - if (didExtract) { - await this.storageRepository.unlink(extractedPath); + if (shouldConvertFullsize) { + // unset size to decode fullsize image + decodeOptions.size = undefined; + fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, image.fullsize.format); + } + + if (shouldExtractEmbedded) { + // For RAW files, try extracting embedded preview first + // Assume extracted image from RAW always in JPEG format, as implied from the `jpgFromRaw` tag name + const extractedPath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, ImageFormat.JPEG); + const didExtract = await this.mediaRepository.extract(asset.originalPath, extractedPath); + useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size)); + + if (useExtracted) { + if (shouldConvertFullsize) { + // skip re-encoding and directly use extracted as fullsize preview + // as usually the extracted image is already heavily compressed, no point doing lossy conversion again + fullsizePath = extractedPath; + } + // use this as origin of preview and thumbnail + decodeInputPath = extractedPath; + if (asset.exifInfo) { + // write essential orientation and colorspace EXIF for correct fullsize preview and subsequent processing + const exif = { orientation: asset.exifInfo.orientation, colorspace: asset.exifInfo.colorspace }; + await this.mediaRepository.writeExif(exif, extractedPath); + } } } + + const { info, data } = await this.mediaRepository.decodeImage(decodeInputPath, decodeOptions); + + const thumbnailOptions = { colorspace, processInvalidImages, raw: info }; + const promises = [ + this.mediaRepository.generateThumbhash(data, thumbnailOptions), + this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailPath), + this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewPath), + ]; + + // did not extract a usable image from RAW + if (fullsizePath && !useExtracted) { + const fullsizeOptions: GenerateThumbnailOptions = { + ...imageFullsizeConfig, + ...thumbnailOptions, + size: undefined, + }; + promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath)); + } + const outputs = await Promise.all(promises); + + return { previewPath, thumbnailPath, fullsizePath, thumbhash: outputs[0] as Buffer }; } private async generateVideoThumbnails(asset: AssetEntity) { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 8ad3c27b4d..3d3d10540b 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -4,9 +4,9 @@ import { OnJob } from 'src/decorators'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; -import { OnThisDayData } from 'src/entities/memory.entity'; import { JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { OnThisDayData } from 'src/types'; import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util'; const DAYS = 3; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 229b63f20e..a0d1cdb4b4 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1,5 +1,6 @@ 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'; @@ -21,14 +22,8 @@ describe(MetadataService.name, () => { let mocks: ServiceMocks; const mockReadTags = (exifData?: Partial, sidecarData?: Partial) => { - exifData = { - FileSize: '123456', - FileCreateDate: '2024-01-01T00:00:00.000Z', - FileModifyDate: '2024-01-01T00:00:00.000Z', - ...exifData, - }; mocks.metadata.readTags.mockReset(); - mocks.metadata.readTags.mockResolvedValueOnce(exifData); + mocks.metadata.readTags.mockResolvedValueOnce(exifData ?? {}); mocks.metadata.readTags.mockResolvedValueOnce(sidecarData ?? {}); }; @@ -114,6 +109,17 @@ describe(MetadataService.name, () => { }); describe('handleMetadataExtraction', () => { + beforeEach(() => { + const time = new Date('2022-01-01T00:00:00.000Z'); + const timeMs = time.valueOf(); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: time, + mtimeMs: timeMs, + birthtimeMs: timeMs, + } as Stats); + }); + it('should handle an asset that could not be found', async () => { await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); @@ -145,10 +151,13 @@ describe(MetadataService.name, () => { const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z'); mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - mockReadTags({ - FileCreateDate: fileCreatedAt.toISOString(), - FileModifyDate: fileModifiedAt.toISOString(), - }); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: fileModifiedAt, + mtimeMs: fileModifiedAt.valueOf(), + birthtimeMs: fileCreatedAt.valueOf(), + } as Stats); + mockReadTags(); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); @@ -168,10 +177,13 @@ describe(MetadataService.name, () => { const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z'); mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - mockReadTags({ - FileCreateDate: fileCreatedAt.toISOString(), - FileModifyDate: fileModifiedAt.toISOString(), - }); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: fileModifiedAt, + mtimeMs: fileModifiedAt.valueOf(), + birthtimeMs: fileCreatedAt.valueOf(), + } as Stats); + mockReadTags(); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); @@ -206,10 +218,14 @@ describe(MetadataService.name, () => { it('should handle lists of numbers', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: assetStub.image.fileModifiedAt, + mtimeMs: assetStub.image.fileModifiedAt.valueOf(), + birthtimeMs: assetStub.image.fileCreatedAt.valueOf(), + } as Stats); mockReadTags({ ISO: [160], - FileCreateDate: assetStub.image.fileCreatedAt.toISOString(), - FileModifyDate: assetStub.image.fileModifiedAt.toISOString(), }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -228,11 +244,15 @@ describe(MetadataService.name, () => { mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]); mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } }); mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' }); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: assetStub.withLocation.fileModifiedAt, + mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(), + birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(), + } as Stats); mockReadTags({ GPSLatitude: assetStub.withLocation.exifInfo!.latitude!, GPSLongitude: assetStub.withLocation.exifInfo!.longitude!, - FileCreateDate: assetStub.withLocation.fileCreatedAt.toISOString(), - FileModifyDate: assetStub.withLocation.fileModifiedAt.toISOString(), }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -475,6 +495,12 @@ describe(MetadataService.name, () => { it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, + mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), + birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + } as Stats); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -483,8 +509,6 @@ describe(MetadataService.name, () => { // instead of the EmbeddedVideoFile, since HEIC MotionPhotos include both EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoType: 'MotionPhoto_Data', - FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(), - FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(), }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); @@ -525,14 +549,18 @@ describe(MetadataService.name, () => { }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, + mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), + birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + } as Stats); mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); mockReadTags({ Directory: 'foo/bar/', EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoType: 'MotionPhoto_Data', MotionPhoto: 1, - FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(), - FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(), }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); @@ -574,13 +602,17 @@ describe(MetadataService.name, () => { it('should extract the motion photo video from the XMP directory entry ', async () => { mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, + mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), + birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + } as Stats); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, MicroVideo: 1, MicroVideoOffset: 1, - FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(), - FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(), }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 4bf58a57fa..1e17f63283 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored'; +import { ContainerDirectoryItem, Maybe, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import { Insertable } from 'kysely'; import _ from 'lodash'; import { Duration } from 'luxon'; +import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import path from 'node:path'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; @@ -77,6 +78,11 @@ const validateRange = (value: number | undefined, min: number, max: number): Non type ImmichTagsWithFaces = ImmichTags & { RegionInfo: NonNullable }; +type Dates = { + dateTimeOriginal: Date; + localDateTime: Date; +}; + @Injectable() export class MetadataService extends BaseService { @OnEvent({ name: 'app.bootstrap', workers: [ImmichWorker.MICROSERVICES] }) @@ -171,18 +177,13 @@ export class MetadataService extends BaseService { return JobStatus.FAILED; } - const exifTags = await this.getExifTags(asset); - if (!exifTags.FileCreateDate || !exifTags.FileModifyDate || exifTags.FileSize === undefined) { - this.logger.warn(`Missing file creation or modification date for asset ${asset.id}: ${asset.originalPath}`); - const stat = await this.storageRepository.stat(asset.originalPath); - exifTags.FileCreateDate = stat.ctime.toISOString(); - exifTags.FileModifyDate = stat.mtime.toISOString(); - exifTags.FileSize = stat.size.toString(); - } - + const [exifTags, stats] = await Promise.all([ + this.getExifTags(asset), + this.storageRepository.stat(asset.originalPath), + ]); this.logger.verbose('Exif Tags', exifTags); - const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags); + const dates = this.getDates(asset, exifTags, stats); const { width, height } = this.getImageDimensions(exifTags); let geo: ReverseGeocodeResult, latitude: number | null, longitude: number | null; @@ -200,9 +201,9 @@ export class MetadataService extends BaseService { assetId: asset.id, // dates - dateTimeOriginal, - modifyDate, - timeZone, + dateTimeOriginal: dates.dateTimeOriginal, + modifyDate: stats.mtime, + timeZone: dates.timeZone, // gps latitude, @@ -212,7 +213,7 @@ export class MetadataService extends BaseService { city: geo.city, // image/file - fileSizeInByte: Number.parseInt(exifTags.FileSize!), + fileSizeInByte: stats.size, exifImageHeight: validate(height), exifImageWidth: validate(width), orientation: validate(exifTags.Orientation)?.toString() ?? null, @@ -245,15 +246,15 @@ export class MetadataService extends BaseService { this.assetRepository.update({ id: asset.id, duration: exifTags.Duration?.toString() ?? null, - localDateTime, - fileCreatedAt: exifData.dateTimeOriginal ?? undefined, - fileModifiedAt: exifData.modifyDate ?? undefined, + localDateTime: dates.localDateTime, + fileCreatedAt: dates.dateTimeOriginal ?? undefined, + fileModifiedAt: stats.mtime, }), this.applyTagList(asset, exifTags), ]; if (this.isMotionPhoto(asset, exifTags)) { - promises.push(this.applyMotionPhotos(asset, exifTags, exifData.fileSizeInByte!)); + promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats)); } if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) { @@ -432,7 +433,7 @@ export class MetadataService extends BaseService { return asset.type === AssetType.IMAGE && !!(tags.MotionPhoto || tags.MicroVideo); } - private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags, fileSize: number) { + private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags, dates: Dates, stats: Stats) { const isMotionPhoto = tags.MotionPhoto; const isMicroVideo = tags.MicroVideo; const videoOffset = tags.MicroVideoOffset; @@ -466,7 +467,7 @@ export class MetadataService extends BaseService { this.logger.debug(`Starting motion photo video extraction for asset ${asset.id}: ${asset.originalPath}`); try { - const position = fileSize - length - padding; + const position = stats.size - length - padding; let video: Buffer; // Samsung MotionPhoto video extraction // HEIC-encoded @@ -505,13 +506,12 @@ export class MetadataService extends BaseService { } } else { const motionAssetId = this.cryptoRepository.randomUUID(); - const dates = this.getDates(asset, tags); motionAsset = await this.assetRepository.create({ id: motionAssetId, libraryId: asset.libraryId, type: AssetType.VIDEO, fileCreatedAt: dates.dateTimeOriginal, - fileModifiedAt: dates.modifyDate, + fileModifiedAt: stats.mtime, localDateTime: dates.localDateTime, checksum, ownerId: asset.ownerId, @@ -634,7 +634,7 @@ export class MetadataService extends BaseService { } } - private getDates(asset: AssetEntity, exifTags: ImmichTags) { + private getDates(asset: AssetEntity, exifTags: ImmichTags, stats: Stats) { const dateTime = firstDateTime(exifTags as Maybe, EXIF_DATE_TAGS); this.logger.verbose(`Date and time is ${dateTime} for asset ${asset.id}: ${asset.originalPath}`); @@ -654,17 +654,16 @@ export class MetadataService extends BaseService { this.logger.debug(`No timezone information found for asset ${asset.id}: ${asset.originalPath}`); } - const modifyDate = this.toDate(exifTags.FileModifyDate!); let dateTimeOriginal = dateTime?.toDate(); let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate(); if (!localDateTime || !dateTimeOriginal) { - const fileCreatedAt = this.toDate(exifTags.FileCreateDate!); - const earliestDate = this.earliestDate(fileCreatedAt, modifyDate); + // FileCreateDate is not available on linux, likely because exiftool hasn't integrated the statx syscall yet + // birthtime is not available in Docker on macOS, so it appears as 0 + const earliestDate = stats.birthtimeMs ? new Date(Math.min(stats.mtimeMs, stats.birthtimeMs)) : stats.mtime; this.logger.debug( - `No exif date time found, falling back on ${earliestDate.toISOString()}, earliest of file creation and modification for assset ${asset.id}: ${asset.originalPath}`, + `No exif date time found, falling back on ${earliestDate.toISOString()}, earliest of file creation and modification for asset ${asset.id}: ${asset.originalPath}`, ); - dateTimeOriginal = earliestDate; - localDateTime = earliestDate; + dateTimeOriginal = localDateTime = earliestDate; } this.logger.verbose( @@ -675,18 +674,9 @@ export class MetadataService extends BaseService { dateTimeOriginal, timeZone, localDateTime, - modifyDate, }; } - private toDate(date: string | ExifDateTime): Date { - return typeof date === 'string' ? new Date(date) : date.toDate(); - } - - private earliestDate(a: Date, b: Date) { - return new Date(Math.min(a.valueOf(), b.valueOf())); - } - private hasGeo(tags: ImmichTags): tags is ImmichTags & { GPSLatitude: number; GPSLongitude: number } { return ( tags.GPSLatitude !== undefined && diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 0deb3805e5..89f211b297 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -265,7 +265,7 @@ describe(NotificationService.name, () => { await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, - data: { baseUrl: 'http://localhost:2283', displayName: userStub.admin.name }, + data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); expect(mocks.notification.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ @@ -306,7 +306,7 @@ describe(NotificationService.name, () => { ).resolves.not.toThrow(); expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, - data: { baseUrl: 'http://localhost:2283', displayName: userStub.admin.name }, + data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); expect(mocks.notification.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index bc6f6b8c2f..dcec865f89 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -2,12 +2,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent, OnJob } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; -import { JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { EmailImageAttachment, IEntityJob, INotifyAlbumUpdateJob, JobItem, JobOf } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFile } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; import { getExternalDomain } from 'src/utils/misc'; import { isEqualObject } from 'src/utils/object'; @@ -146,11 +146,10 @@ export class NotificationService extends BaseService { } const { server } = await this.getConfig({ withCache: false }); - const { port } = this.configRepository.getEnv(); const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.TEST_EMAIL, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), displayName: user.name, }, customTemplate: tempTemplate!, @@ -170,7 +169,6 @@ export class NotificationService extends BaseService { async getTemplate(name: EmailTemplate, customTemplate: string) { const { server, templates } = await this.getConfig({ withCache: false }); - const { port } = this.configRepository.getEnv(); let templateResponse = ''; @@ -179,7 +177,7 @@ export class NotificationService extends BaseService { const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), displayName: 'John Doe', username: 'john@doe.com', password: 'thisIsAPassword123', @@ -194,7 +192,7 @@ export class NotificationService extends BaseService { const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), albumId: '1', albumName: 'Favorite Photos', recipientName: 'Jane Doe', @@ -210,7 +208,7 @@ export class NotificationService extends BaseService { const { html } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), albumId: '1', albumName: "John Doe's Favorites", senderName: 'John Doe', @@ -239,11 +237,10 @@ export class NotificationService extends BaseService { } const { server, templates } = await this.getConfig({ withCache: true }); - const { port } = this.configRepository.getEnv(); const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), displayName: user.name, username: user.email, password: tempPassword, @@ -285,11 +282,10 @@ export class NotificationService extends BaseService { const attachment = await this.getAlbumThumbnailAttachment(album); const { server, templates } = await this.getConfig({ withCache: false }); - const { port } = this.configRepository.getEnv(); const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), albumId: album.id, albumName: album.albumName, senderName: album.owner.name, @@ -332,7 +328,6 @@ export class NotificationService extends BaseService { const attachment = await this.getAlbumThumbnailAttachment(album); const { server, templates } = await this.getConfig({ withCache: false }); - const { port } = this.configRepository.getEnv(); for (const recipient of recipients) { const user = await this.userRepository.get(recipient.id, { withDeleted: false }); @@ -349,7 +344,7 @@ export class NotificationService extends BaseService { const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { - baseUrl: getExternalDomain(server, port), + baseUrl: getExternalDomain(server), albumId: album.id, albumName: album.albumName, recipientName: recipient.name, @@ -403,7 +398,11 @@ export class NotificationService extends BaseService { } const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true }); - const { thumbnailFile } = getAssetFiles(albumThumbnail?.files); + if (!albumThumbnail) { + return; + } + + const thumbnailFile = getAssetFile(albumThumbnail.files, AssetFileType.THUMBNAIL); if (!thumbnailFile) { return; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index e297910a95..b34b0ddcff 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { OnJob } from 'src/decorators'; +import { Chunked, OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -26,6 +26,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { FaceSearchEntity } from 'src/entities/face-search.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { + AssetFileType, AssetType, CacheControl, ImageFormat, @@ -42,7 +43,7 @@ import { BoundingBox } from 'src/repositories/machine-learning.repository'; import { UpdateFacesData } from 'src/repositories/person.repository'; import { BaseService } from 'src/services/base.service'; import { CropOptions, ImageDimensions, InputDimensions, JobItem, JobOf } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFile } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc'; @@ -241,6 +242,7 @@ export class PersonService extends BaseService { return results; } + @Chunked() private async delete(people: PersonEntity[]) { await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath))); await this.personRepository.delete(people); @@ -299,7 +301,7 @@ export class PersonService extends BaseService { const relations = { exifInfo: true, faces: { person: false, withDeleted: true }, files: true }; const [asset] = await this.assetRepository.getByIds([id], relations); - const { previewFile } = getAssetFiles(asset.files); + const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); if (!asset || !previewFile) { return JobStatus.FAILED; } @@ -451,11 +453,11 @@ export class PersonService extends BaseService { return JobStatus.SKIPPED; } - const face = await this.personRepository.getFaceByIdWithAssets( - id, - { person: true, asset: true, faceSearch: true }, - ['id', 'personId', 'sourceType'], - ); + const face = await this.personRepository.getFaceByIdWithAssets(id, { faceSearch: true }, [ + 'id', + 'personId', + 'sourceType', + ]); if (!face || !face.asset) { this.logger.warn(`Face ${id} not found`); return JobStatus.FAILED; @@ -673,7 +675,7 @@ export class PersonService extends BaseService { throw new Error(`Asset ${asset.id} dimensions are unknown`); } - const { previewFile } = getAssetFiles(asset.files); + const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); if (!previewFile) { throw new Error(`Asset ${asset.id} has no preview path`); } diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 79f3a77ebe..51c6b55e11 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -1,3 +1,4 @@ +import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchService } from 'src/services/search.service'; @@ -15,6 +16,7 @@ describe(SearchService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(SearchService)); + mocks.partner.getAll.mockResolvedValue([]); }); it('should work', () => { @@ -155,4 +157,83 @@ describe(SearchService.name, () => { expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); }); + + describe('searchSmart', () => { + beforeEach(() => { + mocks.search.searchSmart.mockResolvedValue({ hasNextPage: false, items: [] }); + mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]'); + }); + + it('should raise a BadRequestException if machine learning is disabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + machineLearning: { enabled: false }, + }); + + await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( + new BadRequestException('Smart search is not enabled'), + ); + }); + + it('should raise a BadRequestException if smart search is disabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + machineLearning: { clip: { enabled: false } }, + }); + + await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( + new BadRequestException('Smart search is not enabled'), + ); + }); + + it('should work', async () => { + await sut.searchSmart(authStub.user1, { query: 'test' }); + + expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( + [expect.any(String)], + 'test', + expect.objectContaining({ modelName: expect.any(String) }), + ); + expect(mocks.search.searchSmart).toHaveBeenCalledWith( + { page: 1, size: 100 }, + { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] }, + ); + }); + + it('should consider page and size parameters', async () => { + await sut.searchSmart(authStub.user1, { query: 'test', page: 2, size: 50 }); + + expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( + [expect.any(String)], + 'test', + expect.objectContaining({ modelName: expect.any(String) }), + ); + expect(mocks.search.searchSmart).toHaveBeenCalledWith( + { page: 2, size: 50 }, + expect.objectContaining({ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] }), + ); + }); + + it('should use clip model specified in config', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + machineLearning: { clip: { modelName: 'ViT-B-16-SigLIP__webli' } }, + }); + + await sut.searchSmart(authStub.user1, { query: 'test' }); + + expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( + [expect.any(String)], + 'test', + expect.objectContaining({ modelName: 'ViT-B-16-SigLIP__webli' }), + ); + }); + + it('should use language specified in request', async () => { + await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' }); + + expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( + [expect.any(String)], + 'test', + expect.objectContaining({ language: 'de' }), + ); + }); + }); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index e2ad9e7f99..1c0c0ad490 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -78,12 +78,10 @@ export class SearchService extends BaseService { } const userIds = await this.getUserIdsToSearch(auth); - - const embedding = await this.machineLearningRepository.encodeText( - machineLearning.urls, - dto.query, - machineLearning.clip, - ); + const embedding = await this.machineLearningRepository.encodeText(machineLearning.urls, dto.query, { + modelName: machineLearning.clip.modelName, + language: dto.language, + }); const page = dto.page ?? 1; const size = dto.size || 100; const { hasNextPage, items } = await this.searchRepository.searchSmart( diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 557fdd5780..8c79f752b7 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -309,7 +309,7 @@ describe(SharedLinkService.name, () => { mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual); await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '1 shared photos & videos', - imageUrl: `http://localhost:2283/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, + imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, title: 'Public Share', }); expect(mocks.sharedLink.get).toHaveBeenCalled(); @@ -319,7 +319,7 @@ describe(SharedLinkService.name, () => { mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] }); await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '0 shared photos & videos', - imageUrl: `http://localhost:2283/feature-panel.png`, + imageUrl: `https://my.immich.app/feature-panel.png`, title: 'Public Share', }); expect(mocks.sharedLink.get).toHaveBeenCalled(); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 74595bb9a2..95f8cef5f8 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -180,7 +180,6 @@ export class SharedLinkService extends BaseService { } const config = await this.getConfig({ withCache: true }); - const { port } = this.configRepository.getEnv(); const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id); const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0; @@ -191,7 +190,7 @@ export class SharedLinkService extends BaseService { return { title: sharedLink.album ? sharedLink.album.albumName : 'Public Share', description: sharedLink.description || `${assetCount} shared photos & videos`, - imageUrl: new URL(imagePath, getExternalDomain(config.server, port)).href, + imageUrl: new URL(imagePath, getExternalDomain(config.server)).href, }; } diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 063bb0bd3b..1909d2bb99 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; -import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetFileType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { WithoutProperty } from 'src/repositories/asset.repository'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFile } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -116,7 +116,7 @@ export class SmartInfoService extends BaseService { return JobStatus.SKIPPED; } - const { previewFile } = getAssetFiles(asset.files); + const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); if (!previewFile) { return JobStatus.FAILED; } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 6bfabe2e8c..edff406b48 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -548,4 +548,102 @@ describe(StorageTemplateService.name, () => { expect(mocks.asset.update).not.toHaveBeenCalled(); }); }); + + describe('file rename correctness', () => { + it('should not create double extensions when filename has lower extension', async () => { + const asset = assetStub.storageAsset({ + originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic', + originalFileName: 'IMG_7065.HEIC', + }); + mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); + mocks.user.getList.mockResolvedValue([userStub.storageLabel]); + mocks.move.create.mockResolvedValue({ + id: '123', + entityId: asset.id, + pathType: AssetPathType.ORIGINAL, + oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic', + newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic', + }); + + await sut.handleMigration(); + + expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); + expect(mocks.storage.rename).toHaveBeenCalledWith( + 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic', + 'upload/library/label-1/2022/2022-06-19/IMG_7065.heic', + ); + }); + + it('should not create double extensions when filename has uppercase extension', async () => { + const asset = assetStub.storageAsset({ + originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC', + originalFileName: 'IMG_7065.HEIC', + }); + mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); + mocks.user.getList.mockResolvedValue([userStub.storageLabel]); + mocks.move.create.mockResolvedValue({ + id: '123', + entityId: asset.id, + pathType: AssetPathType.ORIGINAL, + oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC', + newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic', + }); + + await sut.handleMigration(); + + expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); + expect(mocks.storage.rename).toHaveBeenCalledWith( + 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC', + 'upload/library/label-1/2022/2022-06-19/IMG_7065.heic', + ); + }); + + it('should normalize the filename to lowercase (JPEG > jpg)', async () => { + const asset = assetStub.storageAsset({ + originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG', + originalFileName: 'IMG_7065.JPEG', + }); + mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); + mocks.user.getList.mockResolvedValue([userStub.storageLabel]); + mocks.move.create.mockResolvedValue({ + id: '123', + entityId: asset.id, + pathType: AssetPathType.ORIGINAL, + oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG', + newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg', + }); + + await sut.handleMigration(); + + expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); + expect(mocks.storage.rename).toHaveBeenCalledWith( + 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG', + 'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg', + ); + }); + + it('should normalize the filename to lowercase (JPG > jpg)', async () => { + const asset = assetStub.storageAsset({ + originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', + originalFileName: 'IMG_7065.JPG', + }); + mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); + mocks.user.getList.mockResolvedValue([userStub.storageLabel]); + mocks.move.create.mockResolvedValue({ + id: '123', + entityId: asset.id, + pathType: AssetPathType.ORIGINAL, + oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', + newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg', + }); + + await sut.handleMigration(); + + expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); + expect(mocks.storage.rename).toHaveBeenCalledWith( + 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', + 'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg', + ); + }); + }); }); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 1a0d4f4644..07bac9839a 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -220,9 +220,11 @@ export class StorageTemplateService extends BaseService { const { storageLabel, filename } = metadata; try { + const filenameWithoutExtension = path.basename(filename, path.extname(filename)); + const source = asset.originalPath; let extension = path.extname(source).split('.').pop() as string; - const sanitized = sanitize(path.basename(filename, `.${extension}`)); + const sanitized = sanitize(path.basename(filenameWithoutExtension, `.${extension}`)); extension = extension?.toLowerCase(); const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index ca1d9e7921..99d89df099 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common'; import { join } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { SystemFlags } from 'src/entities/system-metadata.entity'; import { DatabaseLock, JobName, JobStatus, QueueName, StorageFolder, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { JobOf } from 'src/types'; +import { JobOf, SystemFlags } from 'src/types'; import { ImmichStartupError } from 'src/utils/misc'; const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 8a06a883c2..936acf27ad 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -146,6 +146,7 @@ const updatedConfig = Object.freeze({ format: ImageFormat.JPEG, quality: 80, }, + fullsize: { enabled: false, format: ImageFormat.JPEG, quality: 80 }, colorspace: Colorspace.P3, extractEmbedded: false, }, diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index ee28a20d4d..869acc269c 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -4,10 +4,10 @@ import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { DatabaseLock, ImmichEnvironment, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; +import { VersionCheckMetadata } from 'src/types'; const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { return { diff --git a/server/src/sql-tools/decorators.ts b/server/src/sql-tools/decorators.ts new file mode 100644 index 0000000000..88b3e4c7d1 --- /dev/null +++ b/server/src/sql-tools/decorators.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { register } from 'src/sql-tools/schema-from-decorators'; +import { + CheckOptions, + ColumnDefaultValue, + ColumnIndexOptions, + ColumnOptions, + ForeignKeyColumnOptions, + GenerateColumnOptions, + IndexOptions, + TableOptions, + UniqueOptions, +} from 'src/sql-tools/types'; + +export const Table = (options: string | TableOptions = {}): ClassDecorator => { + return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } }); +}; + +export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => { + return (object: object, propertyName: string | symbol) => + void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } }); +}; + +export const Index = (options: string | IndexOptions = {}): ClassDecorator => { + return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } }); +}; + +export const Unique = (options: UniqueOptions): ClassDecorator => { + return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } }); +}; + +export const Check = (options: CheckOptions): ClassDecorator => { + return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } }); +}; + +export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => { + return (object: object, propertyName: string | symbol) => + void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } }); +}; + +export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => { + return (object: object, propertyName: string | symbol) => { + register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } }); + }; +}; + +export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { + return Column({ + type: 'timestamp with time zone', + default: () => 'now()', + ...options, + }); +}; + +export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { + return Column({ + type: 'timestamp with time zone', + default: () => 'now()', + ...options, + }); +}; + +export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { + return Column({ + type: 'timestamp with time zone', + nullable: true, + ...options, + }); +}; + +export const PrimaryGeneratedColumn = (options: Omit = {}) => + GeneratedColumn({ type: 'v4', ...options, primary: true }); + +export const PrimaryColumn = (options: Omit = {}) => Column({ ...options, primary: true }); + +export const GeneratedColumn = ({ type = 'v4', ...options }: GenerateColumnOptions): PropertyDecorator => { + const columnType = type === 'v4' || type === 'v7' ? 'uuid' : type; + + let columnDefault: ColumnDefaultValue | undefined; + switch (type) { + case 'v4': { + columnDefault = () => 'uuid_generate_v4()'; + break; + } + + case 'v7': { + columnDefault = () => 'immich_uuid_v7()'; + break; + } + } + + return Column({ + type: columnType, + default: columnDefault, + ...options, + }); +}; + +export const UpdateIdColumn = () => GeneratedColumn({ type: 'v7', nullable: false }); + +const asOptions = (options: string | T): T => { + if (typeof options === 'string') { + return { name: options } as T; + } + + return options; +}; diff --git a/server/src/sql-tools/index.ts b/server/src/sql-tools/index.ts new file mode 100644 index 0000000000..0d3e53df51 --- /dev/null +++ b/server/src/sql-tools/index.ts @@ -0,0 +1 @@ +export * from 'src/sql-tools/public_api'; diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts new file mode 100644 index 0000000000..8b5a36e6a5 --- /dev/null +++ b/server/src/sql-tools/public_api.ts @@ -0,0 +1,6 @@ +export * from 'src/sql-tools/decorators'; +export { schemaDiff } from 'src/sql-tools/schema-diff'; +export { schemaDiffToSql } from 'src/sql-tools/schema-diff-to-sql'; +export { schemaFromDatabase } from 'src/sql-tools/schema-from-database'; +export { schemaFromDecorators } from 'src/sql-tools/schema-from-decorators'; +export * from 'src/sql-tools/types'; diff --git a/server/src/sql-tools/schema-diff-to-sql.spec.ts b/server/src/sql-tools/schema-diff-to-sql.spec.ts new file mode 100644 index 0000000000..c44d87e6bd --- /dev/null +++ b/server/src/sql-tools/schema-diff-to-sql.spec.ts @@ -0,0 +1,473 @@ +import { DatabaseConstraintType, schemaDiffToSql } from 'src/sql-tools'; +import { describe, expect, it } from 'vitest'; + +describe('diffToSql', () => { + describe('table.drop', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'table.drop', + tableName: 'table1', + reason: 'unknown', + }, + ]), + ).toEqual([`DROP TABLE "table1";`]); + }); + }); + + describe('table.create', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'table.create', + tableName: 'table1', + columns: [ + { + tableName: 'table1', + name: 'column1', + type: 'character varying', + nullable: true, + isArray: false, + synchronize: true, + }, + ], + reason: 'unknown', + }, + ]), + ).toEqual([`CREATE TABLE "table1" ("column1" character varying);`]); + }); + + it('should handle a non-nullable column', () => { + expect( + schemaDiffToSql([ + { + type: 'table.create', + tableName: 'table1', + columns: [ + { + tableName: 'table1', + name: 'column1', + type: 'character varying', + isArray: false, + nullable: false, + synchronize: true, + }, + ], + reason: 'unknown', + }, + ]), + ).toEqual([`CREATE TABLE "table1" ("column1" character varying NOT NULL);`]); + }); + + it('should handle a default value', () => { + expect( + schemaDiffToSql([ + { + type: 'table.create', + tableName: 'table1', + columns: [ + { + tableName: 'table1', + name: 'column1', + type: 'character varying', + isArray: false, + nullable: true, + default: 'uuid_generate_v4()', + synchronize: true, + }, + ], + reason: 'unknown', + }, + ]), + ).toEqual([`CREATE TABLE "table1" ("column1" character varying DEFAULT uuid_generate_v4());`]); + }); + + it('should handle an array type', () => { + expect( + schemaDiffToSql([ + { + type: 'table.create', + tableName: 'table1', + columns: [ + { + tableName: 'table1', + name: 'column1', + type: 'character varying', + isArray: true, + nullable: true, + synchronize: true, + }, + ], + reason: 'unknown', + }, + ]), + ).toEqual([`CREATE TABLE "table1" ("column1" character varying[]);`]); + }); + }); + + describe('column.add', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'column.add', + column: { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD "column1" character varying NOT NULL;']); + }); + + it('should add a nullable column', () => { + expect( + schemaDiffToSql([ + { + type: 'column.add', + column: { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD "column1" character varying;']); + }); + + it('should add a column with an enum type', () => { + expect( + schemaDiffToSql([ + { + type: 'column.add', + column: { + name: 'column1', + tableName: 'table1', + type: 'character varying', + enumName: 'table1_column1_enum', + nullable: true, + isArray: false, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD "column1" table1_column1_enum;']); + }); + + it('should add a column that is an array type', () => { + expect( + schemaDiffToSql([ + { + type: 'column.add', + column: { + name: 'column1', + tableName: 'table1', + type: 'boolean', + nullable: true, + isArray: true, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD "column1" boolean[];']); + }); + }); + + describe('column.alter', () => { + it('should make a column nullable', () => { + expect( + schemaDiffToSql([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { nullable: true }, + reason: 'unknown', + }, + ]), + ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" DROP NOT NULL;`]); + }); + + it('should make a column non-nullable', () => { + expect( + schemaDiffToSql([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { nullable: false }, + reason: 'unknown', + }, + ]), + ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET NOT NULL;`]); + }); + + it('should update the default value', () => { + expect( + schemaDiffToSql([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { default: 'uuid_generate_v4()' }, + reason: 'unknown', + }, + ]), + ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT uuid_generate_v4();`]); + }); + }); + + describe('column.drop', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'column.drop', + tableName: 'table1', + columnName: 'column1', + reason: 'unknown', + }, + ]), + ).toEqual([`ALTER TABLE "table1" DROP COLUMN "column1";`]); + }); + }); + + describe('constraint.add', () => { + describe('primary keys', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'constraint.add', + constraint: { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD CONSTRAINT "PK_test" PRIMARY KEY ("id");']); + }); + }); + + describe('foreign keys', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'constraint.add', + constraint: { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_test', + tableName: 'table1', + columnNames: ['parentId'], + referenceColumnNames: ['id'], + referenceTableName: 'table2', + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual([ + 'ALTER TABLE "table1" ADD CONSTRAINT "FK_test" FOREIGN KEY ("parentId") REFERENCES "table2" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;', + ]); + }); + }); + + describe('unique', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'constraint.add', + constraint: { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD CONSTRAINT "UQ_test" UNIQUE ("id");']); + }); + }); + + describe('check', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'constraint.add', + constraint: { + type: DatabaseConstraintType.CHECK, + name: 'CHK_test', + tableName: 'table1', + expression: '"id" IS NOT NULL', + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['ALTER TABLE "table1" ADD CONSTRAINT "CHK_test" CHECK ("id" IS NOT NULL);']); + }); + }); + }); + + describe('constraint.drop', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'constraint.drop', + tableName: 'table1', + constraintName: 'PK_test', + reason: 'unknown', + }, + ]), + ).toEqual([`ALTER TABLE "table1" DROP CONSTRAINT "PK_test";`]); + }); + }); + + describe('index.create', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'index.create', + index: { + name: 'IDX_test', + tableName: 'table1', + columnNames: ['column1'], + unique: false, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['CREATE INDEX "IDX_test" ON "table1" ("column1")']); + }); + + it('should create an unique index', () => { + expect( + schemaDiffToSql([ + { + type: 'index.create', + index: { + name: 'IDX_test', + tableName: 'table1', + columnNames: ['column1'], + unique: true, + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1")']); + }); + + it('should create an index with a custom expression', () => { + expect( + schemaDiffToSql([ + { + type: 'index.create', + index: { + name: 'IDX_test', + tableName: 'table1', + unique: false, + expression: '"id" IS NOT NULL', + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL)']); + }); + + it('should create an index with a where clause', () => { + expect( + schemaDiffToSql([ + { + type: 'index.create', + index: { + name: 'IDX_test', + tableName: 'table1', + columnNames: ['id'], + unique: false, + where: '("id" IS NOT NULL)', + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL)']); + }); + + it('should create an index with a custom expression', () => { + expect( + schemaDiffToSql([ + { + type: 'index.create', + index: { + name: 'IDX_test', + tableName: 'table1', + unique: false, + using: 'gin', + expression: '"id" IS NOT NULL', + synchronize: true, + }, + reason: 'unknown', + }, + ]), + ).toEqual(['CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL)']); + }); + }); + + describe('index.drop', () => { + it('should work', () => { + expect( + schemaDiffToSql([ + { + type: 'index.drop', + indexName: 'IDX_test', + reason: 'unknown', + }, + ]), + ).toEqual([`DROP INDEX "IDX_test";`]); + }); + }); + + describe('comments', () => { + it('should include the reason in a SQL comment', () => { + expect( + schemaDiffToSql( + [ + { + type: 'index.drop', + indexName: 'IDX_test', + reason: 'unknown', + }, + ], + { comments: true }, + ), + ).toEqual([`DROP INDEX "IDX_test"; -- unknown`]); + }); + }); +}); diff --git a/server/src/sql-tools/schema-diff-to-sql.ts b/server/src/sql-tools/schema-diff-to-sql.ts new file mode 100644 index 0000000000..0a537c600b --- /dev/null +++ b/server/src/sql-tools/schema-diff-to-sql.ts @@ -0,0 +1,204 @@ +import { + DatabaseActionType, + DatabaseColumn, + DatabaseColumnChanges, + DatabaseConstraint, + DatabaseConstraintType, + DatabaseIndex, + SchemaDiff, + SchemaDiffToSqlOptions, +} from 'src/sql-tools/types'; + +const asColumnList = (columns: string[]) => + columns + .toSorted() + .map((column) => `"${column}"`) + .join(', '); +const withNull = (column: DatabaseColumn) => (column.nullable ? '' : ' NOT NULL'); +const withDefault = (column: DatabaseColumn) => (column.default ? ` DEFAULT ${column.default}` : ''); +const withAction = (constraint: { onDelete?: DatabaseActionType; onUpdate?: DatabaseActionType }) => + ` ON UPDATE ${constraint.onUpdate ?? DatabaseActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? DatabaseActionType.NO_ACTION}`; + +const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { + if (!comments) { + return ''; + } + + return ` -- ${item.reason}`; +}; + +const asArray = (items: T | T[]): T[] => { + if (Array.isArray(items)) { + return items; + } + + return [items]; +}; + +export const getColumnType = (column: DatabaseColumn) => { + let type = column.enumName || column.type; + if (column.isArray) { + type += '[]'; + } + + return type; +}; + +/** + * Convert schema diffs into SQL statements + */ +export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => { + return items.flatMap((item) => asArray(asSql(item)).map((result) => result + withComments(options.comments, item))); +}; + +const asSql = (item: SchemaDiff): string | string[] => { + switch (item.type) { + case 'table.create': { + return asTableCreate(item.tableName, item.columns); + } + + case 'table.drop': { + return asTableDrop(item.tableName); + } + + case 'column.add': { + return asColumnAdd(item.column); + } + + case 'column.alter': { + return asColumnAlter(item.tableName, item.columnName, item.changes); + } + + case 'column.drop': { + return asColumnDrop(item.tableName, item.columnName); + } + + case 'constraint.add': { + return asConstraintAdd(item.constraint); + } + + case 'constraint.drop': { + return asConstraintDrop(item.tableName, item.constraintName); + } + + case 'index.create': { + return asIndexCreate(item.index); + } + + case 'index.drop': { + return asIndexDrop(item.indexName); + } + + default: { + return []; + } + } +}; + +const asTableCreate = (tableName: string, tableColumns: DatabaseColumn[]): string => { + const columns = tableColumns + .map((column) => `"${column.name}" ${getColumnType(column)}` + withNull(column) + withDefault(column)) + .join(', '); + return `CREATE TABLE "${tableName}" (${columns});`; +}; + +const asTableDrop = (tableName: string): string => { + return `DROP TABLE "${tableName}";`; +}; + +const asColumnAdd = (column: DatabaseColumn): string => { + return ( + `ALTER TABLE "${column.tableName}" ADD "${column.name}" ${getColumnType(column)}` + + withNull(column) + + withDefault(column) + + ';' + ); +}; + +const asColumnAlter = (tableName: string, columnName: string, changes: DatabaseColumnChanges): string[] => { + const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; + const items: string[] = []; + if (changes.nullable !== undefined) { + items.push(changes.nullable ? `${base} DROP NOT NULL;` : `${base} SET NOT NULL;`); + } + + if (changes.default !== undefined) { + items.push(`${base} SET DEFAULT ${changes.default};`); + } + + return items; +}; + +const asColumnDrop = (tableName: string, columnName: string): string => { + return `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}";`; +}; + +const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => { + const base = `ALTER TABLE "${constraint.tableName}" ADD CONSTRAINT "${constraint.name}"`; + switch (constraint.type) { + case DatabaseConstraintType.PRIMARY_KEY: { + const columnNames = asColumnList(constraint.columnNames); + return `${base} PRIMARY KEY (${columnNames});`; + } + + case DatabaseConstraintType.FOREIGN_KEY: { + const columnNames = asColumnList(constraint.columnNames); + const referenceColumnNames = asColumnList(constraint.referenceColumnNames); + return ( + `${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` + + withAction(constraint) + + ';' + ); + } + + case DatabaseConstraintType.UNIQUE: { + const columnNames = asColumnList(constraint.columnNames); + return `${base} UNIQUE (${columnNames});`; + } + + case DatabaseConstraintType.CHECK: { + return `${base} CHECK (${constraint.expression});`; + } + + default: { + return []; + } + } +}; + +const asConstraintDrop = (tableName: string, constraintName: string): string => { + return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`; +}; + +const asIndexCreate = (index: DatabaseIndex): string => { + let sql = `CREATE`; + + if (index.unique) { + sql += ' UNIQUE'; + } + + sql += ` INDEX "${index.name}" ON "${index.tableName}"`; + + if (index.columnNames) { + const columnNames = asColumnList(index.columnNames); + sql += ` (${columnNames})`; + } + + if (index.using && index.using !== 'btree') { + sql += ` USING ${index.using}`; + } + + if (index.expression) { + sql += ` (${index.expression})`; + } + + if (index.where) { + sql += ` WHERE ${index.where}`; + } + + return sql; +}; + +const asIndexDrop = (indexName: string): string => { + return `DROP INDEX "${indexName}";`; +}; diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts new file mode 100644 index 0000000000..2f536cfabd --- /dev/null +++ b/server/src/sql-tools/schema-diff.spec.ts @@ -0,0 +1,635 @@ +import { schemaDiff } from 'src/sql-tools/schema-diff'; +import { + DatabaseActionType, + DatabaseColumn, + DatabaseColumnType, + DatabaseConstraint, + DatabaseConstraintType, + DatabaseIndex, + DatabaseSchema, + DatabaseTable, +} from 'src/sql-tools/types'; +import { describe, expect, it } from 'vitest'; + +const fromColumn = (column: Partial>): DatabaseSchema => { + const tableName = 'table1'; + + return { + name: 'public', + tables: [ + { + name: tableName, + columns: [ + { + name: 'column1', + synchronize: true, + isArray: false, + type: 'character varying', + nullable: false, + ...column, + tableName, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], + }; +}; + +const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { + const tableName = constraint?.tableName || 'table1'; + + return { + name: 'public', + tables: [ + { + name: tableName, + columns: [ + { + name: 'column1', + synchronize: true, + isArray: false, + type: 'character varying', + nullable: false, + tableName, + }, + ], + indexes: [], + constraints: constraint ? [constraint] : [], + synchronize: true, + }, + ], + warnings: [], + }; +}; + +const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { + const tableName = index?.tableName || 'table1'; + + return { + name: 'public', + tables: [ + { + name: tableName, + columns: [ + { + name: 'column1', + synchronize: true, + isArray: false, + type: 'character varying', + nullable: false, + tableName, + }, + ], + indexes: index ? [index] : [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], + }; +}; + +const newSchema = (schema: { + name?: string; + tables: Array<{ + name: string; + columns?: Array<{ + name: string; + type?: DatabaseColumnType; + nullable?: boolean; + isArray?: boolean; + }>; + indexes?: DatabaseIndex[]; + constraints?: DatabaseConstraint[]; + }>; +}): DatabaseSchema => { + const tables: DatabaseTable[] = []; + + for (const table of schema.tables || []) { + const tableName = table.name; + const columns: DatabaseColumn[] = []; + + for (const column of table.columns || []) { + const columnName = column.name; + + columns.push({ + tableName, + name: columnName, + type: column.type || 'character varying', + isArray: column.isArray ?? false, + nullable: column.nullable ?? false, + synchronize: true, + }); + } + + tables.push({ + name: tableName, + columns, + indexes: table.indexes ?? [], + constraints: table.constraints ?? [], + synchronize: true, + }); + } + + return { + name: schema?.name || 'public', + tables, + warnings: [], + }; +}; + +describe('schemaDiff', () => { + it('should work', () => { + const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] })); + expect(diff.items).toEqual([]); + }); + + describe('table', () => { + describe('table.create', () => { + it('should find a missing table', () => { + const column: DatabaseColumn = { + type: 'character varying', + tableName: 'table1', + name: 'column1', + isArray: false, + nullable: false, + synchronize: true, + }; + const diff = schemaDiff( + newSchema({ tables: [{ name: 'table1', columns: [column] }] }), + newSchema({ tables: [] }), + ); + + expect(diff.items).toHaveLength(1); + expect(diff.items[0]).toEqual({ + type: 'table.create', + tableName: 'table1', + columns: [column], + reason: 'missing in target', + }); + }); + }); + + describe('table.drop', () => { + it('should find an extra table', () => { + const diff = schemaDiff( + newSchema({ tables: [] }), + newSchema({ + tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], + }), + { ignoreExtraTables: false }, + ); + + expect(diff.items).toHaveLength(1); + expect(diff.items[0]).toEqual({ + type: 'table.drop', + tableName: 'table1', + reason: 'missing in source', + }); + }); + }); + + it('should skip identical tables', () => { + const diff = schemaDiff( + newSchema({ + tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], + }), + newSchema({ + tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], + }), + ); + + expect(diff.items).toEqual([]); + }); + }); + + describe('column', () => { + describe('column.add', () => { + it('should find a new column', () => { + const diff = schemaDiff( + newSchema({ + tables: [ + { + name: 'table1', + columns: [{ name: 'column1' }, { name: 'column2' }], + }, + ], + }), + newSchema({ + tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], + }), + ); + + expect(diff.items).toEqual([ + { + type: 'column.add', + column: { + tableName: 'table1', + isArray: false, + name: 'column2', + nullable: false, + type: 'character varying', + synchronize: true, + }, + reason: 'missing in target', + }, + ]); + }); + }); + + describe('column.drop', () => { + it('should find an extra column', () => { + const diff = schemaDiff( + newSchema({ + tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], + }), + newSchema({ + tables: [ + { + name: 'table1', + columns: [{ name: 'column1' }, { name: 'column2' }], + }, + ], + }), + ); + + expect(diff.items).toEqual([ + { + type: 'column.drop', + tableName: 'table1', + columnName: 'column2', + reason: 'missing in source', + }, + ]); + }); + }); + + describe('nullable', () => { + it('should make a column nullable', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', nullable: true }), + fromColumn({ name: 'column1', nullable: false }), + ); + + expect(diff.items).toEqual([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { + nullable: true, + }, + reason: 'nullable is different (true vs false)', + }, + ]); + }); + + it('should make a column non-nullable', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', nullable: false }), + fromColumn({ name: 'column1', nullable: true }), + ); + + expect(diff.items).toEqual([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { + nullable: false, + }, + reason: 'nullable is different (false vs true)', + }, + ]); + }); + }); + + describe('default', () => { + it('should set a default value to a function', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }), + fromColumn({ name: 'column1' }), + ); + + expect(diff.items).toEqual([ + { + type: 'column.alter', + tableName: 'table1', + columnName: 'column1', + changes: { + default: 'uuid_generate_v4()', + }, + reason: 'default is different (uuid_generate_v4() vs undefined)', + }, + ]); + }); + + it('should ignore explicit casts for strings', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', type: 'character varying', default: `''` }), + fromColumn({ name: 'column1', type: 'character varying', default: `''::character varying` }), + ); + + expect(diff.items).toEqual([]); + }); + + it('should ignore explicit casts for numbers', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', type: 'bigint', default: `0` }), + fromColumn({ name: 'column1', type: 'bigint', default: `'0'::bigint` }), + ); + + expect(diff.items).toEqual([]); + }); + + it('should ignore explicit casts for enums', () => { + const diff = schemaDiff( + fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `test` }), + fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `'test'::enum1` }), + ); + + expect(diff.items).toEqual([]); + }); + }); + }); + + describe('constraint', () => { + describe('constraint.add', () => { + it('should detect a new constraint', () => { + const diff = schemaDiff( + fromConstraint({ + name: 'PK_test', + type: DatabaseConstraintType.PRIMARY_KEY, + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }), + fromConstraint(), + ); + + expect(diff.items).toEqual([ + { + type: 'constraint.add', + constraint: { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_test', + columnNames: ['id'], + tableName: 'table1', + synchronize: true, + }, + reason: 'missing in target', + }, + ]); + }); + }); + + describe('constraint.drop', () => { + it('should detect an extra constraint', () => { + const diff = schemaDiff( + fromConstraint(), + fromConstraint({ + name: 'PK_test', + type: DatabaseConstraintType.PRIMARY_KEY, + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }), + ); + + expect(diff.items).toEqual([ + { + type: 'constraint.drop', + tableName: 'table1', + constraintName: 'PK_test', + reason: 'missing in source', + }, + ]); + }); + }); + + describe('primary key', () => { + it('should skip identical primary key constraints', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }; + + const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); + + expect(diff.items).toEqual([]); + }); + }); + + describe('foreign key', () => { + it('should skip identical foreign key constraints', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_test', + tableName: 'table1', + columnNames: ['parentId'], + referenceTableName: 'table2', + referenceColumnNames: ['id'], + synchronize: true, + }; + + const diff = schemaDiff(fromConstraint(constraint), fromConstraint(constraint)); + + expect(diff.items).toEqual([]); + }); + + it('should drop and recreate when the column changes', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_test', + tableName: 'table1', + columnNames: ['parentId'], + referenceTableName: 'table2', + referenceColumnNames: ['id'], + synchronize: true, + }; + + const diff = schemaDiff( + fromConstraint(constraint), + fromConstraint({ ...constraint, columnNames: ['parentId2'] }), + ); + + expect(diff.items).toEqual([ + { + constraintName: 'FK_test', + reason: 'columns are different (parentId vs parentId2)', + tableName: 'table1', + type: 'constraint.drop', + }, + { + constraint: { + columnNames: ['parentId'], + name: 'FK_test', + referenceColumnNames: ['id'], + referenceTableName: 'table2', + synchronize: true, + tableName: 'table1', + type: 'foreign-key', + }, + reason: 'columns are different (parentId vs parentId2)', + type: 'constraint.add', + }, + ]); + }); + + it('should drop and recreate when the ON DELETE action changes', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_test', + tableName: 'table1', + columnNames: ['parentId'], + referenceTableName: 'table2', + referenceColumnNames: ['id'], + onDelete: DatabaseActionType.CASCADE, + synchronize: true, + }; + + const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined })); + + expect(diff.items).toEqual([ + { + constraintName: 'FK_test', + reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', + tableName: 'table1', + type: 'constraint.drop', + }, + { + constraint: { + columnNames: ['parentId'], + name: 'FK_test', + referenceColumnNames: ['id'], + referenceTableName: 'table2', + onDelete: DatabaseActionType.CASCADE, + synchronize: true, + tableName: 'table1', + type: 'foreign-key', + }, + reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', + type: 'constraint.add', + }, + ]); + }); + }); + + describe('unique', () => { + it('should skip identical unique constraints', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }; + + const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); + + expect(diff.items).toEqual([]); + }); + }); + + describe('check', () => { + it('should skip identical check constraints', () => { + const constraint: DatabaseConstraint = { + type: DatabaseConstraintType.CHECK, + name: 'CHK_test', + tableName: 'table1', + expression: 'column1 > 0', + synchronize: true, + }; + + const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); + + expect(diff.items).toEqual([]); + }); + }); + }); + + describe('index', () => { + describe('index.create', () => { + it('should detect a new index', () => { + const diff = schemaDiff( + fromIndex({ + name: 'IDX_test', + tableName: 'table1', + columnNames: ['id'], + unique: false, + synchronize: true, + }), + fromIndex(), + ); + + expect(diff.items).toEqual([ + { + type: 'index.create', + index: { + name: 'IDX_test', + columnNames: ['id'], + tableName: 'table1', + unique: false, + synchronize: true, + }, + reason: 'missing in target', + }, + ]); + }); + }); + + describe('index.drop', () => { + it('should detect an extra index', () => { + const diff = schemaDiff( + fromIndex(), + fromIndex({ + name: 'IDX_test', + unique: true, + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }), + ); + + expect(diff.items).toEqual([ + { + type: 'index.drop', + indexName: 'IDX_test', + reason: 'missing in source', + }, + ]); + }); + }); + + it('should recreate the index if unique changes', () => { + const index: DatabaseIndex = { + name: 'IDX_test', + tableName: 'table1', + columnNames: ['id'], + unique: true, + synchronize: true, + }; + const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false })); + + expect(diff.items).toEqual([ + { + type: 'index.drop', + indexName: 'IDX_test', + reason: 'uniqueness is different (true vs false)', + }, + { + type: 'index.create', + index, + reason: 'uniqueness is different (true vs false)', + }, + ]); + }); + }); +}); diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts new file mode 100644 index 0000000000..ca7f35a45f --- /dev/null +++ b/server/src/sql-tools/schema-diff.ts @@ -0,0 +1,449 @@ +import { getColumnType, schemaDiffToSql } from 'src/sql-tools/schema-diff-to-sql'; +import { + DatabaseCheckConstraint, + DatabaseColumn, + DatabaseConstraint, + DatabaseConstraintType, + DatabaseForeignKeyConstraint, + DatabaseIndex, + DatabasePrimaryKeyConstraint, + DatabaseSchema, + DatabaseTable, + DatabaseUniqueConstraint, + SchemaDiff, + SchemaDiffToSqlOptions, +} from 'src/sql-tools/types'; + +enum Reason { + MissingInSource = 'missing in source', + MissingInTarget = 'missing in target', +} + +const setIsEqual = (source: Set, target: Set) => + source.size === target.size && [...source].every((x) => target.has(x)); + +const haveEqualColumns = (sourceColumns?: string[], targetColumns?: string[]) => { + return setIsEqual(new Set(sourceColumns ?? []), new Set(targetColumns ?? [])); +}; + +const isSynchronizeDisabled = (source?: { synchronize?: boolean }, target?: { synchronize?: boolean }) => { + return source?.synchronize === false || target?.synchronize === false; +}; + +const withTypeCast = (value: string, type: string) => { + if (!value.startsWith(`'`)) { + value = `'${value}'`; + } + return `${value}::${type}`; +}; + +const isDefaultEqual = (source: DatabaseColumn, target: DatabaseColumn) => { + if (source.default === target.default) { + return true; + } + + if (source.default === undefined || target.default === undefined) { + return false; + } + + if ( + withTypeCast(source.default, getColumnType(source)) === target.default || + source.default === withTypeCast(target.default, getColumnType(target)) + ) { + return true; + } + + return false; +}; + +/** + * Compute the difference between two database schemas + */ +export const schemaDiff = ( + source: DatabaseSchema, + target: DatabaseSchema, + options: { ignoreExtraTables?: boolean } = {}, +) => { + const items = diffTables(source.tables, target.tables, { + ignoreExtraTables: options.ignoreExtraTables ?? true, + }); + + return { + items, + asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(items, options), + }; +}; + +export const diffTables = ( + sources: DatabaseTable[], + targets: DatabaseTable[], + options: { ignoreExtraTables: boolean }, +) => { + const items: SchemaDiff[] = []; + const sourceMap = Object.fromEntries(sources.map((table) => [table.name, table])); + const targetMap = Object.fromEntries(targets.map((table) => [table.name, table])); + const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); + + for (const key of keys) { + if (options.ignoreExtraTables && !sourceMap[key]) { + continue; + } + items.push(...diffTable(sourceMap[key], targetMap[key])); + } + + return items; +}; + +const diffTable = (source?: DatabaseTable, target?: DatabaseTable): SchemaDiff[] => { + if (isSynchronizeDisabled(source, target)) { + return []; + } + + if (source && !target) { + return [ + { + type: 'table.create', + tableName: source.name, + columns: Object.values(source.columns), + reason: Reason.MissingInTarget, + }, + ...diffIndexes(source.indexes, []), + // TODO merge constraints into table create record when possible + ...diffConstraints(source.constraints, []), + ]; + } + + if (!source && target) { + return [ + { + type: 'table.drop', + tableName: target.name, + reason: Reason.MissingInSource, + }, + ]; + } + + if (!source || !target) { + return []; + } + + return [ + ...diffColumns(source.columns, target.columns), + ...diffConstraints(source.constraints, target.constraints), + ...diffIndexes(source.indexes, target.indexes), + ]; +}; + +const diffColumns = (sources: DatabaseColumn[], targets: DatabaseColumn[]): SchemaDiff[] => { + const items: SchemaDiff[] = []; + const sourceMap = Object.fromEntries(sources.map((column) => [column.name, column])); + const targetMap = Object.fromEntries(targets.map((column) => [column.name, column])); + const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); + + for (const key of keys) { + items.push(...diffColumn(sourceMap[key], targetMap[key])); + } + + return items; +}; + +const diffColumn = (source?: DatabaseColumn, target?: DatabaseColumn): SchemaDiff[] => { + if (isSynchronizeDisabled(source, target)) { + return []; + } + + if (source && !target) { + return [ + { + type: 'column.add', + column: source, + reason: Reason.MissingInTarget, + }, + ]; + } + + if (!source && target) { + return [ + { + type: 'column.drop', + tableName: target.tableName, + columnName: target.name, + reason: Reason.MissingInSource, + }, + ]; + } + + if (!source || !target) { + return []; + } + + const sourceType = getColumnType(source); + const targetType = getColumnType(target); + + const isTypeChanged = sourceType !== targetType; + + if (isTypeChanged) { + // TODO: convert between types via UPDATE when possible + return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`); + } + + const items: SchemaDiff[] = []; + if (source.nullable !== target.nullable) { + items.push({ + type: 'column.alter', + tableName: source.tableName, + columnName: source.name, + changes: { + nullable: source.nullable, + }, + reason: `nullable is different (${source.nullable} vs ${target.nullable})`, + }); + } + + if (!isDefaultEqual(source, target)) { + items.push({ + type: 'column.alter', + tableName: source.tableName, + columnName: source.name, + changes: { + default: String(source.default), + }, + reason: `default is different (${source.default} vs ${target.default})`, + }); + } + + return items; +}; + +const diffConstraints = (sources: DatabaseConstraint[], targets: DatabaseConstraint[]): SchemaDiff[] => { + const items: SchemaDiff[] = []; + + for (const type of Object.values(DatabaseConstraintType)) { + const sourceMap = Object.fromEntries(sources.filter((item) => item.type === type).map((item) => [item.name, item])); + const targetMap = Object.fromEntries(targets.filter((item) => item.type === type).map((item) => [item.name, item])); + const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); + + for (const key of keys) { + items.push(...diffConstraint(sourceMap[key], targetMap[key])); + } + } + + return items; +}; + +const diffConstraint = (source?: T, target?: T): SchemaDiff[] => { + if (isSynchronizeDisabled(source, target)) { + return []; + } + + if (source && !target) { + return [ + { + type: 'constraint.add', + constraint: source, + reason: Reason.MissingInTarget, + }, + ]; + } + + if (!source && target) { + return [ + { + type: 'constraint.drop', + tableName: target.tableName, + constraintName: target.name, + reason: Reason.MissingInSource, + }, + ]; + } + + if (!source || !target) { + return []; + } + + switch (source.type) { + case DatabaseConstraintType.PRIMARY_KEY: { + return diffPrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint); + } + + case DatabaseConstraintType.FOREIGN_KEY: { + return diffForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint); + } + + case DatabaseConstraintType.UNIQUE: { + return diffUniqueConstraint(source, target as DatabaseUniqueConstraint); + } + + case DatabaseConstraintType.CHECK: { + return diffCheckConstraint(source, target as DatabaseCheckConstraint); + } + + default: { + return dropAndRecreateConstraint(source, target, `Unknown constraint type: ${(source as any).type}`); + } + } +}; + +const diffPrimaryKeyConstraint = ( + source: DatabasePrimaryKeyConstraint, + target: DatabasePrimaryKeyConstraint, +): SchemaDiff[] => { + if (!haveEqualColumns(source.columnNames, target.columnNames)) { + return dropAndRecreateConstraint( + source, + target, + `Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`, + ); + } + + return []; +}; + +const diffForeignKeyConstraint = ( + source: DatabaseForeignKeyConstraint, + target: DatabaseForeignKeyConstraint, +): SchemaDiff[] => { + let reason = ''; + + const sourceDeleteAction = source.onDelete ?? 'NO ACTION'; + const targetDeleteAction = target.onDelete ?? 'NO ACTION'; + + const sourceUpdateAction = source.onUpdate ?? 'NO ACTION'; + const targetUpdateAction = target.onUpdate ?? 'NO ACTION'; + + if (!haveEqualColumns(source.columnNames, target.columnNames)) { + reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; + } else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) { + reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`; + } else if (source.referenceTableName !== target.referenceTableName) { + reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`; + } else if (sourceDeleteAction !== targetDeleteAction) { + reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`; + } else if (sourceUpdateAction !== targetUpdateAction) { + reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`; + } + + if (reason) { + return dropAndRecreateConstraint(source, target, reason); + } + + return []; +}; + +const diffUniqueConstraint = (source: DatabaseUniqueConstraint, target: DatabaseUniqueConstraint): SchemaDiff[] => { + let reason = ''; + + if (!haveEqualColumns(source.columnNames, target.columnNames)) { + reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; + } + + if (reason) { + return dropAndRecreateConstraint(source, target, reason); + } + + return []; +}; + +const diffCheckConstraint = (source: DatabaseCheckConstraint, target: DatabaseCheckConstraint): SchemaDiff[] => { + if (source.expression !== target.expression) { + // comparing expressions is hard because postgres reconstructs it with different formatting + // for now if the constraint exists with the same name, we will just skip it + } + + return []; +}; + +const diffIndexes = (sources: DatabaseIndex[], targets: DatabaseIndex[]) => { + const items: SchemaDiff[] = []; + const sourceMap = Object.fromEntries(sources.map((index) => [index.name, index])); + const targetMap = Object.fromEntries(targets.map((index) => [index.name, index])); + const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); + + for (const key of keys) { + items.push(...diffIndex(sourceMap[key], targetMap[key])); + } + + return items; +}; + +const diffIndex = (source?: DatabaseIndex, target?: DatabaseIndex): SchemaDiff[] => { + if (isSynchronizeDisabled(source, target)) { + return []; + } + + if (source && !target) { + return [{ type: 'index.create', index: source, reason: Reason.MissingInTarget }]; + } + + if (!source && target) { + return [ + { + type: 'index.drop', + indexName: target.name, + reason: Reason.MissingInSource, + }, + ]; + } + + if (!target || !source) { + return []; + } + + const sourceUsing = source.using ?? 'btree'; + const targetUsing = target.using ?? 'btree'; + + let reason = ''; + + if (!haveEqualColumns(source.columnNames, target.columnNames)) { + reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; + } else if (source.unique !== target.unique) { + reason = `uniqueness is different (${source.unique} vs ${target.unique})`; + } else if (sourceUsing !== targetUsing) { + reason = `using method is different (${source.using} vs ${target.using})`; + } else if (source.where !== target.where) { + reason = `where clause is different (${source.where} vs ${target.where})`; + } else if (source.expression !== target.expression) { + reason = `expression is different (${source.expression} vs ${target.expression})`; + } + + if (reason) { + return dropAndRecreateIndex(source, target, reason); + } + + return []; +}; + +const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { + return [ + { + type: 'column.drop', + tableName: target.tableName, + columnName: target.name, + reason, + }, + { type: 'column.add', column: source, reason }, + ]; +}; + +const dropAndRecreateConstraint = ( + source: DatabaseConstraint, + target: DatabaseConstraint, + reason: string, +): SchemaDiff[] => { + return [ + { + type: 'constraint.drop', + tableName: target.tableName, + constraintName: target.name, + reason, + }, + { type: 'constraint.add', constraint: source, reason }, + ]; +}; + +const dropAndRecreateIndex = (source: DatabaseIndex, target: DatabaseIndex, reason: string): SchemaDiff[] => { + return [ + { type: 'index.drop', indexName: target.name, reason }, + { type: 'index.create', index: source, reason }, + ]; +}; diff --git a/server/src/sql-tools/schema-from-database.ts b/server/src/sql-tools/schema-from-database.ts new file mode 100644 index 0000000000..fe7af6b623 --- /dev/null +++ b/server/src/sql-tools/schema-from-database.ts @@ -0,0 +1,394 @@ +import { Kysely, sql } from 'kysely'; +import { PostgresJSDialect } from 'kysely-postgres-js'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { Sql } from 'postgres'; +import { + DatabaseActionType, + DatabaseClient, + DatabaseColumn, + DatabaseColumnType, + DatabaseConstraintType, + DatabaseSchema, + DatabaseTable, + LoadSchemaOptions, + PostgresDB, +} from 'src/sql-tools/types'; + +/** + * Load the database schema from the database + */ +export const schemaFromDatabase = async (postgres: Sql, options: LoadSchemaOptions = {}): Promise => { + const db = createDatabaseClient(postgres); + + const warnings: string[] = []; + const warn = (message: string) => { + warnings.push(message); + }; + + const schemaName = options.schemaName || 'public'; + const tablesMap: Record = {}; + + const [tables, columns, indexes, constraints, enums] = await Promise.all([ + getTables(db, schemaName), + getTableColumns(db, schemaName), + getTableIndexes(db, schemaName), + getTableConstraints(db, schemaName), + getUserDefinedEnums(db, schemaName), + ]); + + const enumMap = Object.fromEntries(enums.map((e) => [e.name, e.values])); + + // add tables + for (const table of tables) { + const tableName = table.table_name; + if (tablesMap[tableName]) { + continue; + } + + tablesMap[table.table_name] = { + name: table.table_name, + columns: [], + indexes: [], + constraints: [], + synchronize: true, + }; + } + + // add columns to tables + for (const column of columns) { + const table = tablesMap[column.table_name]; + if (!table) { + continue; + } + + const columnName = column.column_name; + + const item: DatabaseColumn = { + type: column.data_type as DatabaseColumnType, + name: columnName, + tableName: column.table_name, + nullable: column.is_nullable === 'YES', + isArray: column.array_type !== null, + numericPrecision: column.numeric_precision ?? undefined, + numericScale: column.numeric_scale ?? undefined, + default: column.column_default ?? undefined, + synchronize: true, + }; + + const columnLabel = `${table.name}.${columnName}`; + + switch (column.data_type) { + // array types + case 'ARRAY': { + if (!column.array_type) { + warn(`Unable to find type for ${columnLabel} (ARRAY)`); + continue; + } + item.type = column.array_type as DatabaseColumnType; + break; + } + + // enum types + case 'USER-DEFINED': { + if (!enumMap[column.udt_name]) { + warn(`Unable to find type for ${columnLabel} (ENUM)`); + continue; + } + + item.type = 'enum'; + item.enumName = column.udt_name; + item.enumValues = enumMap[column.udt_name]; + break; + } + } + + table.columns.push(item); + } + + // add table indexes + for (const index of indexes) { + const table = tablesMap[index.table_name]; + if (!table) { + continue; + } + + const indexName = index.index_name; + + table.indexes.push({ + name: indexName, + tableName: index.table_name, + columnNames: index.column_names ?? undefined, + expression: index.expression ?? undefined, + using: index.using, + where: index.where ?? undefined, + unique: index.unique, + synchronize: true, + }); + } + + // add table constraints + for (const constraint of constraints) { + const table = tablesMap[constraint.table_name]; + if (!table) { + continue; + } + + const constraintName = constraint.constraint_name; + + switch (constraint.constraint_type) { + // primary key constraint + case 'p': { + if (!constraint.column_names) { + warn(`Skipping CONSTRAINT "${constraintName}", no columns found`); + continue; + } + table.constraints.push({ + type: DatabaseConstraintType.PRIMARY_KEY, + name: constraintName, + tableName: constraint.table_name, + columnNames: constraint.column_names, + synchronize: true, + }); + break; + } + + // foreign key constraint + case 'f': { + if (!constraint.column_names || !constraint.reference_table_name || !constraint.reference_column_names) { + warn( + `Skipping CONSTRAINT "${constraintName}", missing either columns, referenced table, or referenced columns,`, + ); + continue; + } + + table.constraints.push({ + type: DatabaseConstraintType.FOREIGN_KEY, + name: constraintName, + tableName: constraint.table_name, + columnNames: constraint.column_names, + referenceTableName: constraint.reference_table_name, + referenceColumnNames: constraint.reference_column_names, + onUpdate: asDatabaseAction(constraint.update_action), + onDelete: asDatabaseAction(constraint.delete_action), + synchronize: true, + }); + break; + } + + // unique constraint + case 'u': { + table.constraints.push({ + type: DatabaseConstraintType.UNIQUE, + name: constraintName, + tableName: constraint.table_name, + columnNames: constraint.column_names as string[], + synchronize: true, + }); + break; + } + + // check constraint + case 'c': { + table.constraints.push({ + type: DatabaseConstraintType.CHECK, + name: constraint.constraint_name, + tableName: constraint.table_name, + expression: constraint.expression.replace('CHECK ', ''), + synchronize: true, + }); + break; + } + } + } + + await db.destroy(); + + return { + name: schemaName, + tables: Object.values(tablesMap), + warnings, + }; +}; + +const createDatabaseClient = (postgres: Sql): DatabaseClient => + new Kysely({ dialect: new PostgresJSDialect({ postgres }) }); + +const asDatabaseAction = (action: string) => { + switch (action) { + case 'a': { + return DatabaseActionType.NO_ACTION; + } + case 'c': { + return DatabaseActionType.CASCADE; + } + case 'r': { + return DatabaseActionType.RESTRICT; + } + case 'n': { + return DatabaseActionType.SET_NULL; + } + case 'd': { + return DatabaseActionType.SET_DEFAULT; + } + + default: { + return DatabaseActionType.NO_ACTION; + } + } +}; + +const getTables = (db: DatabaseClient, schemaName: string) => { + return db + .selectFrom('information_schema.tables') + .where('table_schema', '=', schemaName) + .where('table_type', '=', sql.lit('BASE TABLE')) + .selectAll() + .execute(); +}; + +const getUserDefinedEnums = async (db: DatabaseClient, schemaName: string) => { + const items = await db + .selectFrom('pg_type') + .innerJoin('pg_namespace', (join) => + join.onRef('pg_namespace.oid', '=', 'pg_type.typnamespace').on('pg_namespace.nspname', '=', schemaName), + ) + .where('typtype', '=', sql.lit('e')) + .select((eb) => [ + 'pg_type.typname as name', + jsonArrayFrom( + eb.selectFrom('pg_enum as e').select(['e.enumlabel as value']).whereRef('e.enumtypid', '=', 'pg_type.oid'), + ).as('values'), + ]) + .execute(); + + return items.map((item) => ({ + name: item.name, + values: item.values.map(({ value }) => value), + })); +}; + +const getTableColumns = (db: DatabaseClient, schemaName: string) => { + return db + .selectFrom('information_schema.columns as c') + .leftJoin('information_schema.element_types as o', (join) => + join + .onRef('c.table_catalog', '=', 'o.object_catalog') + .onRef('c.table_schema', '=', 'o.object_schema') + .onRef('c.table_name', '=', 'o.object_name') + .on('o.object_type', '=', sql.lit('TABLE')) + .onRef('c.dtd_identifier', '=', 'o.collection_type_identifier'), + ) + .leftJoin('pg_type as t', (join) => + join.onRef('t.typname', '=', 'c.udt_name').on('c.data_type', '=', sql.lit('USER-DEFINED')), + ) + .leftJoin('pg_enum as e', (join) => join.onRef('e.enumtypid', '=', 't.oid')) + .select([ + 'c.table_name', + 'c.column_name', + + // is ARRAY, USER-DEFINED, or data type + 'c.data_type', + 'c.column_default', + 'c.is_nullable', + + // number types + 'c.numeric_precision', + 'c.numeric_scale', + + // date types + 'c.datetime_precision', + + // user defined type + 'c.udt_catalog', + 'c.udt_schema', + 'c.udt_name', + + // data type for ARRAYs + 'o.data_type as array_type', + ]) + .where('table_schema', '=', schemaName) + .execute(); +}; + +const getTableIndexes = (db: DatabaseClient, schemaName: string) => { + return ( + db + .selectFrom('pg_index as ix') + // matching index, which has column information + .innerJoin('pg_class as i', 'ix.indexrelid', 'i.oid') + .innerJoin('pg_am as a', 'i.relam', 'a.oid') + // matching table + .innerJoin('pg_class as t', 'ix.indrelid', 't.oid') + // namespace + .innerJoin('pg_namespace', 'pg_namespace.oid', 'i.relnamespace') + // PK and UQ constraints automatically have indexes, so we can ignore those + .leftJoin('pg_constraint', (join) => + join + .onRef('pg_constraint.conindid', '=', 'i.oid') + .on('pg_constraint.contype', 'in', [sql.lit('p'), sql.lit('u')]), + ) + .where('pg_constraint.oid', 'is', null) + .select((eb) => [ + 'i.relname as index_name', + 't.relname as table_name', + 'ix.indisunique as unique', + 'a.amname as using', + eb.fn('pg_get_expr', ['ix.indexprs', 'ix.indrelid']).as('expression'), + eb.fn('pg_get_expr', ['ix.indpred', 'ix.indrelid']).as('where'), + eb + .selectFrom('pg_attribute as a') + .where('t.relkind', '=', sql.lit('r')) + .whereRef('a.attrelid', '=', 't.oid') + // list of columns numbers in the index + .whereRef('a.attnum', '=', sql`any("ix"."indkey")`) + .select((eb) => eb.fn('json_agg', ['a.attname']).as('column_name')) + .as('column_names'), + ]) + .where('pg_namespace.nspname', '=', schemaName) + .where('ix.indisprimary', '=', sql.lit(false)) + .execute() + ); +}; + +const getTableConstraints = (db: DatabaseClient, schemaName: string) => { + return db + .selectFrom('pg_constraint') + .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_constraint.connamespace') // namespace + .innerJoin('pg_class as source_table', (join) => + join.onRef('source_table.oid', '=', 'pg_constraint.conrelid').on('source_table.relkind', 'in', [ + // ordinary table + sql.lit('r'), + // partitioned table + sql.lit('p'), + // foreign table + sql.lit('f'), + ]), + ) // table + .leftJoin('pg_class as reference_table', 'reference_table.oid', 'pg_constraint.confrelid') // reference table + .select((eb) => [ + 'pg_constraint.contype as constraint_type', + 'pg_constraint.conname as constraint_name', + 'source_table.relname as table_name', + 'reference_table.relname as reference_table_name', + 'pg_constraint.confupdtype as update_action', + 'pg_constraint.confdeltype as delete_action', + // 'pg_constraint.oid as constraint_id', + eb + .selectFrom('pg_attribute') + // matching table for PK, FK, and UQ + .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.conrelid') + .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."conkey")`) + .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) + .as('column_names'), + eb + .selectFrom('pg_attribute') + // matching foreign table for FK + .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.confrelid') + .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."confkey")`) + .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) + .as('reference_column_names'), + eb.fn('pg_get_constraintdef', ['pg_constraint.oid']).as('expression'), + ]) + .where('pg_namespace.nspname', '=', schemaName) + .execute(); +}; diff --git a/server/src/sql-tools/schema-from-decorators.spec.ts b/server/src/sql-tools/schema-from-decorators.spec.ts new file mode 100644 index 0000000000..6703277844 --- /dev/null +++ b/server/src/sql-tools/schema-from-decorators.spec.ts @@ -0,0 +1,31 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { reset, schemaFromDecorators } from 'src/sql-tools/schema-from-decorators'; +import { describe, expect, it } from 'vitest'; + +describe('schemaDiff', () => { + beforeEach(() => { + reset(); + }); + + it('should work', () => { + expect(schemaFromDecorators()).toEqual({ + name: 'public', + tables: [], + warnings: [], + }); + }); + + describe('test files', () => { + const files = readdirSync('test/sql-tools', { withFileTypes: true }); + for (const file of files) { + const filePath = join(file.parentPath, file.name); + it(filePath, async () => { + const module = await import(filePath); + expect(module.description).toBeDefined(); + expect(module.schema).toBeDefined(); + expect(schemaFromDecorators(), module.description).toEqual(module.schema); + }); + } + }); +}); diff --git a/server/src/sql-tools/schema-from-decorators.ts b/server/src/sql-tools/schema-from-decorators.ts new file mode 100644 index 0000000000..b11817678e --- /dev/null +++ b/server/src/sql-tools/schema-from-decorators.ts @@ -0,0 +1,443 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { createHash } from 'node:crypto'; +import 'reflect-metadata'; +import { + CheckOptions, + ColumnDefaultValue, + ColumnIndexOptions, + ColumnOptions, + DatabaseActionType, + DatabaseColumn, + DatabaseConstraintType, + DatabaseSchema, + DatabaseTable, + ForeignKeyColumnOptions, + IndexOptions, + TableOptions, + UniqueOptions, +} from 'src/sql-tools/types'; + +enum SchemaKey { + TableName = 'immich-schema:table-name', + ColumnName = 'immich-schema:column-name', + IndexName = 'immich-schema:index-name', +} + +type SchemaTable = DatabaseTable & { options: TableOptions }; +type SchemaTables = SchemaTable[]; +type ClassBased = { object: Function } & T; +type PropertyBased = { object: object; propertyName: string | symbol } & T; +type RegisterItem = + | { type: 'table'; item: ClassBased<{ options: TableOptions }> } + | { type: 'index'; item: ClassBased<{ options: IndexOptions }> } + | { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> } + | { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> } + | { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> } + | { type: 'columnIndex'; item: PropertyBased<{ options: ColumnIndexOptions }> } + | { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> }; + +const items: RegisterItem[] = []; +export const register = (item: RegisterItem) => void items.push(item); + +const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); +const asKey = (prefix: string, tableName: string, values: string[]) => + (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); +const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); +const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns); +const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); +const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); +const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); +const asIndexName = (table: string, columns: string[] | undefined, where: string | undefined) => { + const items: string[] = []; + for (const columnName of columns ?? []) { + items.push(columnName); + } + + if (where) { + items.push(where); + } + + return asKey('IDX_', table, items); +}; + +const makeColumn = ({ + name, + tableName, + options, +}: { + name: string; + tableName: string; + options: ColumnOptions; +}): DatabaseColumn => { + const columnName = options.name ?? name; + const enumName = options.enumName ?? `${tableName}_${columnName}_enum`.toLowerCase(); + let defaultValue = asDefaultValue(options); + let nullable = options.nullable ?? false; + + if (defaultValue === null) { + nullable = true; + defaultValue = undefined; + } + + const isEnum = !!options.enum; + + return { + name: columnName, + tableName, + primary: options.primary ?? false, + default: defaultValue, + nullable, + enumName: isEnum ? enumName : undefined, + enumValues: isEnum ? Object.values(options.enum as object) : undefined, + isArray: options.array ?? false, + type: isEnum ? 'enum' : options.type || 'character varying', + synchronize: options.synchronize ?? true, + }; +}; + +const asDefaultValue = (options: { nullable?: boolean; default?: ColumnDefaultValue }) => { + if (typeof options.default === 'function') { + return options.default() as string; + } + + if (options.default === undefined) { + return; + } + + const value = options.default; + + if (value === null) { + return value; + } + + if (typeof value === 'number') { + return String(value); + } + + if (typeof value === 'boolean') { + return value ? 'true' : 'false'; + } + + if (value instanceof Date) { + return `'${value.toISOString()}'`; + } + + return `'${String(value)}'`; +}; + +const missingTableError = (context: string, object: object, propertyName?: string | symbol) => { + const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); + return `[${context}] Unable to find table (${label})`; +}; + +// match TypeORM +const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); + +const findByName = (items: T[], name?: string) => + name ? items.find((item) => item.name === name) : undefined; +const resolveTable = (tables: SchemaTables, object: object) => + findByName(tables, Reflect.getMetadata(SchemaKey.TableName, object)); + +let initialized = false; +let schema: DatabaseSchema; + +export const reset = () => { + initialized = false; + items.length = 0; +}; + +export const schemaFromDecorators = () => { + if (!initialized) { + const schemaTables: SchemaTables = []; + + const warnings: string[] = []; + const warn = (message: string) => void warnings.push(message); + + for (const { item } of items.filter((item) => item.type === 'table')) { + processTable(schemaTables, item); + } + + for (const { item } of items.filter((item) => item.type === 'column')) { + processColumn(schemaTables, item, { warn }); + } + + for (const { item } of items.filter((item) => item.type === 'foreignKeyColumn')) { + processForeignKeyColumn(schemaTables, item, { warn }); + } + + for (const { item } of items.filter((item) => item.type === 'uniqueConstraint')) { + processUniqueConstraint(schemaTables, item, { warn }); + } + + for (const { item } of items.filter((item) => item.type === 'checkConstraint')) { + processCheckConstraint(schemaTables, item, { warn }); + } + + for (const table of schemaTables) { + processPrimaryKeyConstraint(table); + } + + for (const { item } of items.filter((item) => item.type === 'index')) { + processIndex(schemaTables, item, { warn }); + } + + for (const { item } of items.filter((item) => item.type === 'columnIndex')) { + processColumnIndex(schemaTables, item, { warn }); + } + + for (const { item } of items.filter((item) => item.type === 'foreignKeyColumn')) { + processForeignKeyConstraint(schemaTables, item, { warn }); + } + + schema = { + name: 'public', + tables: schemaTables.map(({ options: _, ...table }) => table), + warnings, + }; + + initialized = true; + } + + return schema; +}; + +const processTable = (tables: SchemaTables, { object, options }: ClassBased<{ options: TableOptions }>) => { + const tableName = options.name || asSnakeCase(object.name); + Reflect.defineMetadata(SchemaKey.TableName, tableName, object); + tables.push({ + name: tableName, + columns: [], + constraints: [], + indexes: [], + options, + synchronize: options.synchronize ?? true, + }); +}; + +type OnWarn = (message: string) => void; + +const processColumn = ( + tables: SchemaTables, + { object, propertyName, options }: PropertyBased<{ options: ColumnOptions }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object.constructor); + if (!table) { + warn(missingTableError('@Column', object, propertyName)); + return; + } + + // TODO make sure column name is unique + + const column = makeColumn({ name: String(propertyName), tableName: table.name, options }); + + Reflect.defineMetadata(SchemaKey.ColumnName, column.name, object, propertyName); + + table.columns.push(column); + + if (!options.primary && options.unique) { + table.constraints.push({ + type: DatabaseConstraintType.UNIQUE, + name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), + tableName: table.name, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); + } +}; + +const processUniqueConstraint = ( + tables: SchemaTables, + { object, options }: ClassBased<{ options: UniqueOptions }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object); + if (!table) { + warn(missingTableError('@Unique', object)); + return; + } + + const tableName = table.name; + const columnNames = options.columns; + + table.constraints.push({ + type: DatabaseConstraintType.UNIQUE, + name: options.name || asUniqueConstraintName(tableName, columnNames), + tableName, + columnNames, + synchronize: options.synchronize ?? true, + }); +}; + +const processCheckConstraint = ( + tables: SchemaTables, + { object, options }: ClassBased<{ options: CheckOptions }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object); + if (!table) { + warn(missingTableError('@Check', object)); + return; + } + + const tableName = table.name; + + table.constraints.push({ + type: DatabaseConstraintType.CHECK, + name: options.name || asCheckConstraintName(tableName, options.expression), + tableName, + expression: options.expression, + synchronize: options.synchronize ?? true, + }); +}; + +const processPrimaryKeyConstraint = (table: SchemaTable) => { + const columnNames: string[] = []; + + for (const column of table.columns) { + if (column.primary) { + columnNames.push(column.name); + } + } + + if (columnNames.length > 0) { + table.constraints.push({ + type: DatabaseConstraintType.PRIMARY_KEY, + name: table.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames), + tableName: table.name, + columnNames, + synchronize: table.options.synchronize ?? true, + }); + } +}; + +const processIndex = ( + tables: SchemaTables, + { object, options }: ClassBased<{ options: IndexOptions }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object); + if (!table) { + warn(missingTableError('@Index', object)); + return; + } + + table.indexes.push({ + name: options.name || asIndexName(table.name, options.columns, options.where), + tableName: table.name, + unique: options.unique ?? false, + expression: options.expression, + using: options.using, + where: options.where, + columnNames: options.columns, + synchronize: options.synchronize ?? true, + }); +}; + +const processColumnIndex = ( + tables: SchemaTables, + { object, propertyName, options }: PropertyBased<{ options: ColumnIndexOptions }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object.constructor); + if (!table) { + warn(missingTableError('@ColumnIndex', object, propertyName)); + return; + } + + const column = findByName(table.columns, Reflect.getMetadata(SchemaKey.ColumnName, object, propertyName)); + if (!column) { + return; + } + + table.indexes.push({ + name: options.name || asIndexName(table.name, [column.name], options.where), + tableName: table.name, + unique: options.unique ?? false, + expression: options.expression, + using: options.using, + where: options.where, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); +}; + +const processForeignKeyColumn = ( + tables: SchemaTables, + { object, propertyName, options }: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }>, + { warn }: { warn: OnWarn }, +) => { + const table = resolveTable(tables, object.constructor); + if (!table) { + warn(missingTableError('@ForeignKeyColumn', object)); + return; + } + + const columnName = String(propertyName); + const existingColumn = table.columns.find((column) => column.name === columnName); + if (existingColumn) { + // TODO log warnings if column options and `@Column` is also used + return; + } + + const column = makeColumn({ name: columnName, tableName: table.name, options }); + + Reflect.defineMetadata(SchemaKey.ColumnName, columnName, object, propertyName); + + table.columns.push(column); +}; + +const processForeignKeyConstraint = ( + tables: SchemaTables, + { object, propertyName, options, target }: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }>, + { warn }: { warn: OnWarn }, +) => { + const childTable = resolveTable(tables, object.constructor); + if (!childTable) { + warn(missingTableError('@ForeignKeyColumn', object)); + return; + } + + const parentTable = resolveTable(tables, target()); + if (!parentTable) { + warn(missingTableError('@ForeignKeyColumn', object, propertyName)); + return; + } + + const columnName = String(propertyName); + const column = childTable.columns.find((column) => column.name === columnName); + if (!column) { + warn('@ForeignKeyColumn: Column not found, creating a new one'); + return; + } + + const columnNames = [column.name]; + const referenceColumns = parentTable.columns.filter((column) => column.primary); + + // infer FK column type from reference table + if (referenceColumns.length === 1) { + column.type = referenceColumns[0].type; + } + + childTable.constraints.push({ + name: options.constraintName || asForeignKeyConstraintName(childTable.name, columnNames), + tableName: childTable.name, + columnNames, + type: DatabaseConstraintType.FOREIGN_KEY, + referenceTableName: parentTable.name, + referenceColumnNames: referenceColumns.map((column) => column.name), + onUpdate: options.onUpdate as DatabaseActionType, + onDelete: options.onDelete as DatabaseActionType, + synchronize: options.synchronize ?? true, + }); + + if (options.unique) { + childTable.constraints.push({ + name: options.uniqueConstraintName || asRelationKeyConstraintName(childTable.name, columnNames), + tableName: childTable.name, + columnNames, + type: DatabaseConstraintType.UNIQUE, + synchronize: options.synchronize ?? true, + }); + } +}; diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts new file mode 100644 index 0000000000..64813ca348 --- /dev/null +++ b/server/src/sql-tools/types.ts @@ -0,0 +1,363 @@ +import { Kysely } from 'kysely'; + +export type PostgresDB = { + pg_am: { + oid: number; + amname: string; + amhandler: string; + amtype: string; + }; + + pg_attribute: { + attrelid: number; + attname: string; + attnum: number; + atttypeid: number; + attstattarget: number; + attstatarget: number; + aanum: number; + }; + + pg_class: { + oid: number; + relname: string; + relkind: string; + relnamespace: string; + reltype: string; + relowner: string; + relam: string; + relfilenode: string; + reltablespace: string; + relpages: number; + reltuples: number; + relallvisible: number; + reltoastrelid: string; + relhasindex: PostgresYesOrNo; + relisshared: PostgresYesOrNo; + relpersistence: string; + }; + + pg_constraint: { + oid: number; + conname: string; + conrelid: string; + contype: string; + connamespace: string; + conkey: number[]; + confkey: number[]; + confrelid: string; + confupdtype: string; + confdeltype: string; + confmatchtype: number; + condeferrable: PostgresYesOrNo; + condeferred: PostgresYesOrNo; + convalidated: PostgresYesOrNo; + conindid: number; + }; + + pg_enum: { + oid: string; + enumtypid: string; + enumsortorder: number; + enumlabel: string; + }; + + pg_index: { + indexrelid: string; + indrelid: string; + indisready: boolean; + indexprs: string | null; + indpred: string | null; + indkey: number[]; + indisprimary: boolean; + indisunique: boolean; + }; + + pg_indexes: { + schemaname: string; + tablename: string; + indexname: string; + tablespace: string | null; + indexrelid: string; + indexdef: string; + }; + + pg_namespace: { + oid: number; + nspname: string; + nspowner: number; + nspacl: string[]; + }; + + pg_type: { + oid: string; + typname: string; + typnamespace: string; + typowner: string; + typtype: string; + typcategory: string; + typarray: string; + }; + + 'information_schema.tables': { + table_catalog: string; + table_schema: string; + table_name: string; + table_type: 'VIEW' | 'BASE TABLE' | string; + is_insertable_info: PostgresYesOrNo; + is_typed: PostgresYesOrNo; + commit_action: string | null; + }; + + 'information_schema.columns': { + table_catalog: string; + table_schema: string; + table_name: string; + column_name: string; + ordinal_position: number; + column_default: string | null; + is_nullable: PostgresYesOrNo; + data_type: string; + dtd_identifier: string; + character_maximum_length: number | null; + character_octet_length: number | null; + numeric_precision: number | null; + numeric_precision_radix: number | null; + numeric_scale: number | null; + datetime_precision: number | null; + interval_type: string | null; + interval_precision: number | null; + udt_catalog: string; + udt_schema: string; + udt_name: string; + maximum_cardinality: number | null; + is_updatable: PostgresYesOrNo; + }; + + 'information_schema.element_types': { + object_catalog: string; + object_schema: string; + object_name: string; + object_type: string; + collection_type_identifier: string; + data_type: string; + }; +}; + +type PostgresYesOrNo = 'YES' | 'NO'; + +export type ColumnDefaultValue = null | boolean | string | number | object | Date | (() => string); + +export type DatabaseClient = Kysely; + +export enum DatabaseConstraintType { + PRIMARY_KEY = 'primary-key', + FOREIGN_KEY = 'foreign-key', + UNIQUE = 'unique', + CHECK = 'check', +} + +export enum DatabaseActionType { + NO_ACTION = 'NO ACTION', + RESTRICT = 'RESTRICT', + CASCADE = 'CASCADE', + SET_NULL = 'SET NULL', + SET_DEFAULT = 'SET DEFAULT', +} + +export type DatabaseColumnType = + | 'bigint' + | 'boolean' + | 'bytea' + | 'character' + | 'character varying' + | 'date' + | 'double precision' + | 'integer' + | 'jsonb' + | 'polygon' + | 'text' + | 'time' + | 'time with time zone' + | 'time without time zone' + | 'timestamp' + | 'timestamp with time zone' + | 'timestamp without time zone' + | 'uuid' + | 'vector' + | 'enum' + | 'serial'; + +export type TableOptions = { + name?: string; + primaryConstraintName?: string; + synchronize?: boolean; +}; + +type ColumnBaseOptions = { + name?: string; + primary?: boolean; + type?: DatabaseColumnType; + nullable?: boolean; + length?: number; + default?: ColumnDefaultValue; + synchronize?: boolean; +}; + +export type ColumnOptions = ColumnBaseOptions & { + enum?: object; + enumName?: string; + array?: boolean; + unique?: boolean; + uniqueConstraintName?: string; +}; + +export type GenerateColumnOptions = Omit & { + type?: 'v4' | 'v7'; +}; + +export type ColumnIndexOptions = { + name?: string; + unique?: boolean; + expression?: string; + using?: string; + where?: string; + synchronize?: boolean; +}; + +export type IndexOptions = ColumnIndexOptions & { + columns?: string[]; + synchronize?: boolean; +}; + +export type UniqueOptions = { + name?: string; + columns: string[]; + synchronize?: boolean; +}; + +export type CheckOptions = { + name?: string; + expression: string; + synchronize?: boolean; +}; + +export type DatabaseSchema = { + name: string; + tables: DatabaseTable[]; + warnings: string[]; +}; + +export type DatabaseTable = { + name: string; + columns: DatabaseColumn[]; + indexes: DatabaseIndex[]; + constraints: DatabaseConstraint[]; + synchronize: boolean; +}; + +export type DatabaseConstraint = + | DatabasePrimaryKeyConstraint + | DatabaseForeignKeyConstraint + | DatabaseUniqueConstraint + | DatabaseCheckConstraint; + +export type DatabaseColumn = { + primary?: boolean; + name: string; + tableName: string; + + type: DatabaseColumnType; + nullable: boolean; + isArray: boolean; + synchronize: boolean; + + default?: string; + length?: number; + + // enum values + enumValues?: string[]; + enumName?: string; + + // numeric types + numericPrecision?: number; + numericScale?: number; +}; + +export type DatabaseColumnChanges = { + nullable?: boolean; + default?: string; +}; + +type ColumBasedConstraint = { + name: string; + tableName: string; + columnNames: string[]; +}; + +export type DatabasePrimaryKeyConstraint = ColumBasedConstraint & { + type: DatabaseConstraintType.PRIMARY_KEY; + synchronize: boolean; +}; + +export type DatabaseUniqueConstraint = ColumBasedConstraint & { + type: DatabaseConstraintType.UNIQUE; + synchronize: boolean; +}; + +export type DatabaseForeignKeyConstraint = ColumBasedConstraint & { + type: DatabaseConstraintType.FOREIGN_KEY; + referenceTableName: string; + referenceColumnNames: string[]; + onUpdate?: DatabaseActionType; + onDelete?: DatabaseActionType; + synchronize: boolean; +}; + +export type DatabaseCheckConstraint = { + type: DatabaseConstraintType.CHECK; + name: string; + tableName: string; + expression: string; + synchronize: boolean; +}; + +export type DatabaseIndex = { + name: string; + tableName: string; + columnNames?: string[]; + expression?: string; + unique: boolean; + using?: string; + where?: string; + synchronize: boolean; +}; + +export type LoadSchemaOptions = { + schemaName?: string; +}; + +export type SchemaDiffToSqlOptions = { + comments?: boolean; +}; + +export type SchemaDiff = { reason: string } & ( + | { type: 'table.create'; tableName: string; columns: DatabaseColumn[] } + | { type: 'table.drop'; tableName: string } + | { type: 'column.add'; column: DatabaseColumn } + | { type: 'column.alter'; tableName: string; columnName: string; changes: DatabaseColumnChanges } + | { type: 'column.drop'; tableName: string; columnName: string } + | { type: 'constraint.add'; constraint: DatabaseConstraint } + | { type: 'constraint.drop'; tableName: string; constraintName: string } + | { type: 'index.create'; index: DatabaseIndex } + | { type: 'index.drop'; indexName: string } +); + +type Action = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION'; +export type ForeignKeyColumnOptions = ColumnBaseOptions & { + onUpdate?: Action; + onDelete?: Action; + constraintName?: string; + unique?: boolean; + uniqueConstraintName?: string; +}; diff --git a/server/src/subscribers/audit.subscriber.ts b/server/src/subscribers/audit.subscriber.ts deleted file mode 100644 index 8c2ad3e18d..0000000000 --- a/server/src/subscribers/audit.subscriber.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { AuditEntity } from 'src/entities/audit.entity'; -import { DatabaseAction, EntityType } from 'src/enum'; -import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm'; - -@EventSubscriber() -export class AuditSubscriber implements EntitySubscriberInterface { - async afterRemove(event: RemoveEvent): Promise { - await this.onEvent(DatabaseAction.DELETE, event); - } - - private async onEvent(action: DatabaseAction, event: RemoveEvent): Promise { - const audit = this.getAudit(event.metadata.name, { ...event.entity, id: event.entityId }); - if (audit && audit.entityId && audit.ownerId) { - await event.manager.getRepository(AuditEntity).save({ ...audit, action }); - } - } - - private getAudit(entityName: string, entity: any): Partial | null { - switch (entityName) { - case AssetEntity.name: { - const asset = entity as AssetEntity; - return { - entityType: EntityType.ASSET, - entityId: asset.id, - ownerId: asset.ownerId, - }; - } - - case AlbumEntity.name: { - const album = entity as AlbumEntity; - return { - entityType: EntityType.ALBUM, - entityId: album.id, - ownerId: album.ownerId, - }; - } - } - - return null; - } -} diff --git a/server/src/types.ts b/server/src/types.ts index 1c0a61b259..623b0fece0 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,11 +1,15 @@ +import { SystemConfig } from 'src/config'; import { AssetType, DatabaseExtension, ExifOrientation, ImageFormat, JobName, + MemoryType, QueueName, + StorageFolder, SyncEntityType, + SystemMetadataKey, TranscodeTarget, VideoCodec, } from 'src/enum'; @@ -54,6 +58,12 @@ export interface CropOptions { height: number; } +export interface FullsizeImageOptions { + format: ImageFormat; + quality: number; + enabled: boolean; +} + export interface ImageOptions { format: ImageFormat; quality: number; @@ -74,11 +84,11 @@ interface DecodeImageOptions { } export interface DecodeToBufferOptions extends DecodeImageOptions { - size: number; + size?: number; orientation?: ExifOrientation; } -export type GenerateThumbnailOptions = ImageOptions & DecodeImageOptions; +export type GenerateThumbnailOptions = Pick & DecodeToBufferOptions; export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo }; @@ -454,3 +464,27 @@ export type StorageAsset = { sidecarPath: string | null; fileSizeInByte: number | null; }; + +export type OnThisDayData = { year: number }; + +export interface MemoryData { + [MemoryType.ON_THIS_DAY]: OnThisDayData; +} + +export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; +export type SystemFlags = { mountChecks: Record }; +export type MemoriesState = { + /** memories have already been created through this date */ + lastOnThisDayDate: string; +}; + +export interface SystemMetadata extends Record> { + [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean }; + [SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string }; + [SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; + [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; + [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial; + [SystemMetadataKey.SYSTEM_FLAGS]: DeepPartial; + [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; + [SystemMetadataKey.MEMORIES_STATE]: MemoriesState; +} diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index e88c8e1a63..4e21a9226e 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -250,10 +250,6 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return access.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.MEMORY_DELETE: { - return access.memory.checkOwnerAccess(auth.user.id, ids); - } - case Permission.PERSON_READ: { return await access.person.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index de64720a82..96ef90bfce 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { StorageCore } from 'src/cores/storage.core'; +import { GeneratedImageType, StorageCore } from 'src/cores/storage.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -13,13 +13,14 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { IBulkAsset, ImmichFile, UploadFile } from 'src/types'; import { checkAccess } from 'src/utils/access'; -const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => { +export const getAssetFile = (files: AssetFileEntity[], type: AssetFileType | GeneratedImageType) => { return (files || []).find((file) => file.type === type); }; -export const getAssetFiles = (files?: AssetFileEntity[]) => ({ - previewFile: getFileByType(files, AssetFileType.PREVIEW), - thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL), +export const getAssetFiles = (files: AssetFileEntity[]) => ({ + fullsizeFile: getAssetFile(files, AssetFileType.FULLSIZE), + previewFile: getAssetFile(files, AssetFileType.PREVIEW), + thumbnailFile: getAssetFile(files, AssetFileType.THUMBNAIL), }); export const addAssets = async ( diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 456165063c..8e07f388a0 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,19 +1,4 @@ import { Expression, sql } from 'kysely'; -import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; - -/** - * Allows optional values unlike the regular Between and uses MoreThanOrEqual - * or LessThanOrEqual when only one parameter is specified. - */ -export function OptionalBetween(from?: T, to?: T) { - if (from && to) { - return Between(from, to); - } else if (from) { - return MoreThanOrEqual(from); - } else if (to) { - return LessThanOrEqual(to); - } -} export const asUuid = (id: string | Expression) => sql`${id}::uuid`; @@ -32,16 +17,3 @@ export const removeUndefinedKeys = (update: T, template: unkno return update; }; - -/** - * Mainly for type debugging to make VS Code display a more useful tooltip. - * Source: https://stackoverflow.com/a/69288824 - */ -export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; - -/** Recursive version of {@link Expand} from the same source. */ -export type ExpandRecursively = T extends object - ? T extends infer O - ? { [K in keyof O]: ExpandRecursively } - : never - : T; diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index f2f47e0471..ecc8847043 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,6 +1,5 @@ import { HttpException } from '@nestjs/common'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { TypeORMError } from 'typeorm'; export const logGlobalError = (logger: LoggingRepository, error: Error) => { if (error instanceof HttpException) { @@ -10,11 +9,6 @@ export const logGlobalError = (logger: LoggingRepository, error: Error) => { return; } - if (error instanceof TypeORMError) { - logger.error(`Database error: ${error}`); - return; - } - if (error instanceof Error) { logger.error(`Unknown error: ${error}`, error?.stack); return; diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index d26f8d0fb6..b00eb652ef 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -128,6 +128,8 @@ export class BaseConfig implements VideoCodecSWConfig { '-fps_mode passthrough', // explicitly selects the video stream instead of leaving it up to FFmpeg `-map 0:${videoStream.index}`, + // Strip metadata like capture date, camera, and GPS + '-map_metadata -1', ]; if (audioStream) { diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 37dfe8153a..7beeb91b67 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -55,6 +55,20 @@ const image: Record = { '.webp': ['image/webp'], }; +/** + * list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg + * @TODO share with the client + * @see {@link web/src/lib/utils/asset-utils.ts#L329} + **/ +const webSupportedImageMimeTypes = new Set([ + 'image/apng', + 'image/avif', + 'image/gif', + 'image/jpeg', + 'image/png', + 'image/webp', +]); + const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); const profile: Record = Object.fromEntries( Object.entries(image).filter(([key]) => profileExtensions.has(key)), @@ -100,6 +114,7 @@ export const mimeTypes = { isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), + isWebSupportedImage: (filename: string) => webSupportedImageMimeTypes.has(lookup(filename)), isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index b0c4fd955f..ff1656da74 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -44,8 +44,7 @@ export const getMethodNames = (instance: any) => { return methods; }; -export const getExternalDomain = (server: SystemConfig['server'], port: number) => - server.externalDomain || `http://localhost:${port}`; +export const getExternalDomain = (server: SystemConfig['server']) => server.externalDomain || `https://my.immich.app`; /** * @returns a list of strings representing the keys of the object in dot notation diff --git a/server/test/factory.ts b/server/test/factory.ts index 69160aa8a4..028b530255 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -1,7 +1,7 @@ import { Insertable, Kysely } from 'kysely'; import { randomBytes } from 'node:crypto'; import { Writable } from 'node:stream'; -import { Assets, DB, Partners, Sessions, Users } from 'src/db'; +import { Assets, DB, Partners, Sessions } from 'src/db'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; @@ -35,6 +35,7 @@ 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 { UserTable } from 'src/schema/tables/user.table'; import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { newUuid } from 'test/small.factory'; import { automock } from 'test/utils'; @@ -57,7 +58,7 @@ class CustomWritable extends Writable { } type Asset = Partial>; -type User = Partial>; +type User = Partial>; type Session = Omit, 'token'> & { token?: string }; type Partner = Insertable; @@ -103,7 +104,7 @@ export class TestFactory { static user(user: User = {}) { const userId = user.id || newUuid(); - const defaults: Insertable = { + const defaults: Insertable = { email: `${userId}@immich.cloud`, name: `User ${userId}`, deletedAt: null, @@ -215,6 +216,7 @@ export class TestContext { view: ViewRepository; private constructor(public db: Kysely) { + // eslint-disable-next-line no-sparse-arrays const logger = automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }); const config = new ConfigRepository(); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index c0902dddb3..d56c5f6efd 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -26,7 +26,16 @@ const thumbnailFile: AssetFileEntity = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), }; -const files: AssetFileEntity[] = [previewFile, thumbnailFile]; +const fullsizeFile: AssetFileEntity = { + id: 'file-3', + assetId: 'asset-id', + type: AssetFileType.FULLSIZE, + path: '/uploads/user-id/fullsize/path.webp', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const files: AssetFileEntity[] = [fullsizeFile, previewFile, thumbnailFile]; export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => { return { @@ -553,6 +562,7 @@ export const assetStub = { fileSizeInByte: 25_000, timeZone: `America/New_York`, }, + files, } as AssetEntity), livePhotoWithOriginalFileName: Object.freeze({ @@ -789,7 +799,47 @@ export const assetStub = { livePhotoVideoId: null, tags: [], sharedLinks: [], - originalFileName: 'asset-id.jpg', + originalFileName: 'asset-id.dng', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + } as ExifEntity, + duplicateId: null, + isOffline: false, + }), + + imageHif: Object.freeze({ + id: 'asset-id', + status: AssetStatus.ACTIVE, + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.hif', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + files, + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + duration: null, + isVisible: true, + isExternal: false, + livePhotoVideo: null, + livePhotoVideoId: null, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.hif', faces: [], deletedAt: null, sidecarPath: null, diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts deleted file mode 100644 index 24f78a17ce..0000000000 --- a/server/test/fixtures/audit.stub.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AuditEntity } from 'src/entities/audit.entity'; -import { DatabaseAction, EntityType } from 'src/enum'; -import { authStub } from 'test/fixtures/auth.stub'; - -export const auditStub = { - delete: Object.freeze({ - id: 3, - entityId: 'asset-deleted', - action: DatabaseAction.DELETE, - entityType: EntityType.ASSET, - ownerId: authStub.admin.user.id, - createdAt: new Date(), - }), -}; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 9153cfa8f2..0ed1502fb9 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -17,7 +17,6 @@ export const userStub = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - tags: [], assets: [], metadata: [], quotaSizeInBytes: null, @@ -36,7 +35,6 @@ export const userStub = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - tags: [], assets: [], metadata: [ { @@ -62,7 +60,6 @@ export const userStub = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - tags: [], assets: [], quotaSizeInBytes: null, quotaUsageInBytes: 0, @@ -81,7 +78,6 @@ export const userStub = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - tags: [], assets: [], quotaSizeInBytes: null, quotaUsageInBytes: 0, @@ -100,7 +96,6 @@ export const userStub = { createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - tags: [], assets: [], quotaSizeInBytes: null, quotaUsageInBytes: 0, diff --git a/server/test/medium/globalSetup.ts b/server/test/medium/globalSetup.ts index 3c25142073..e8aa8f9ee8 100644 --- a/server/test/medium/globalSetup.ts +++ b/server/test/medium/globalSetup.ts @@ -1,8 +1,14 @@ +import { FileMigrationProvider, Kysely, Migrator } from 'kysely'; +import { PostgresJSDialect } from 'kysely-postgres-js'; +import { mkdir, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import { parse } from 'pg-connection-string'; +import postgres, { Notice } from 'postgres'; import { GenericContainer, Wait } from 'testcontainers'; import { DataSource } from 'typeorm'; const globalSetup = async () => { - const postgres = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') + const postgresContainer = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: 'postgres', @@ -29,7 +35,7 @@ const globalSetup = async () => { .withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)])) .start(); - const postgresPort = postgres.getMappedPort(5432); + const postgresPort = postgresContainer.getMappedPort(5432); const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`; process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl; @@ -55,6 +61,73 @@ const globalSetup = async () => { await dataSource.initialize(); await dataSource.runMigrations(); await dataSource.destroy(); + + // for whatever reason, importing from test/utils causes vitest to crash + // eslint-disable-next-line unicorn/prefer-module + const migrationFolder = join(__dirname, '..', 'schema/migrations'); + // TODO remove after we have at least one kysely migration + await mkdir(migrationFolder, { recursive: true }); + + const parsed = parse(process.env.IMMICH_TEST_POSTGRES_URL!); + + const parsedOptions = { + ...parsed, + ssl: false, + host: parsed.host ?? undefined, + port: parsed.port ? Number(parsed.port) : undefined, + database: parsed.database ?? undefined, + }; + + const driverOptions = { + ...parsedOptions, + onnotice: (notice: Notice) => { + if (notice['severity'] !== 'NOTICE') { + console.warn('Postgres notice:', notice); + } + }, + max: 10, + types: { + date: { + to: 1184, + from: [1082, 1114, 1184], + serialize: (x: Date | string) => (x instanceof Date ? x.toISOString() : x), + parse: (x: string) => new Date(x), + }, + bigint: { + to: 20, + from: [20], + parse: (value: string) => Number.parseInt(value), + serialize: (value: number) => value.toString(), + }, + }, + connection: { + TimeZone: 'UTC', + }, + }; + + const db = new Kysely({ + dialect: new PostgresJSDialect({ postgres: postgres({ ...driverOptions, max: 1, database: 'postgres' }) }), + }); + + // TODO just call `databaseRepository.migrate()` (probably have to wait until TypeOrm is gone) + const migrator = new Migrator({ + db, + migrationLockTableName: 'kysely_migrations_lock', + migrationTableName: 'kysely_migrations', + provider: new FileMigrationProvider({ + fs: { readdir }, + path: { join }, + migrationFolder, + }), + }); + + const { error } = await migrator.migrateToLatest(); + if (error) { + console.error('Unable to run kysely migrations', error); + throw error; + } + + await db.destroy(); }; export default globalSetup; diff --git a/server/test/medium/specs/metadata.service.spec.ts b/server/test/medium/specs/metadata.service.spec.ts index 28f2c9f64f..fbdb8b51ac 100644 --- a/server/test/medium/specs/metadata.service.spec.ts +++ b/server/test/medium/specs/metadata.service.spec.ts @@ -9,6 +9,7 @@ import { MetadataService } from 'src/services/metadata.service'; import { automock, newRandomImage, newTestService, ServiceMocks } from 'test/utils'; const metadataRepository = new MetadataRepository( + // eslint-disable-next-line no-sparse-arrays automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }), ); @@ -38,7 +39,12 @@ describe(MetadataService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(MetadataService, { metadata: metadataRepository })); - mocks.storage.stat.mockResolvedValue({ size: 123_456, ctime: new Date(), mtime: new Date() } as Stats); + mocks.storage.stat.mockResolvedValue({ + size: 123_456, + mtime: new Date(654_321), + mtimeMs: 654_321, + birthtimeMs: 654_322, + } as Stats); delete process.env.TZ; }); @@ -53,8 +59,6 @@ describe(MetadataService.name, () => { description: 'should handle no time zone information', exifData: { DateTimeOriginal: '2022:01:01 00:00:00', - FileCreateDate: '2022:01:01 00:00:00', - FileModifyDate: '2022:01:01 00:00:00', }, expected: { localDateTime: '2022-01-01T00:00:00.000Z', @@ -67,8 +71,6 @@ describe(MetadataService.name, () => { serverTimeZone: 'America/Los_Angeles', exifData: { DateTimeOriginal: '2022:01:01 00:00:00', - FileCreateDate: '2022:01:01 00:00:00', - FileModifyDate: '2022:01:01 00:00:00', }, expected: { localDateTime: '2022-01-01T00:00:00.000Z', @@ -81,8 +83,6 @@ describe(MetadataService.name, () => { serverTimeZone: 'Europe/Brussels', exifData: { DateTimeOriginal: '2022:01:01 00:00:00', - FileCreateDate: '2022:01:01 00:00:00', - FileModifyDate: '2022:01:01 00:00:00', }, expected: { localDateTime: '2022-01-01T00:00:00.000Z', @@ -95,8 +95,6 @@ describe(MetadataService.name, () => { serverTimeZone: 'Europe/Brussels', exifData: { DateTimeOriginal: '2022:06:01 00:00:00', - FileCreateDate: '2022:06:01 00:00:00', - FileModifyDate: '2022:06:01 00:00:00', }, expected: { localDateTime: '2022-06-01T00:00:00.000Z', @@ -108,8 +106,6 @@ describe(MetadataService.name, () => { description: 'should handle a +13:00 time zone', exifData: { DateTimeOriginal: '2022:01:01 00:00:00+13:00', - FileCreateDate: '2022:01:01 00:00:00+13:00', - FileModifyDate: '2022:01:01 00:00:00+13:00', }, expected: { localDateTime: '2022-01-01T00:00:00.000Z', diff --git a/server/test/medium/specs/user.service.spec.ts b/server/test/medium/specs/user.service.spec.ts index fded6ec3b2..5a91c64994 100644 --- a/server/test/medium/specs/user.service.spec.ts +++ b/server/test/medium/specs/user.service.spec.ts @@ -13,7 +13,7 @@ const setup = async (db: Kysely) => { return { sut, mocks, context }; }; -describe.concurrent(UserService.name, () => { +describe(UserService.name, () => { let sut: UserService; let context: TestContext; let mocks: ServiceMocks; @@ -124,22 +124,31 @@ describe.concurrent(UserService.name, () => { }); }); - describe('handleUserDeleteCheck', () => { + describe.sequential('handleUserDeleteCheck', () => { + beforeEach(async () => { + // These tests specifically have to be sequential otherwise we hit race conditions with config changes applying in incorrect tests + const config = await sut.getConfig({ withCache: false }); + config.user.deleteDelay = 7; + await sut.updateConfig(config); + }); + it('should work when there are no deleted users', async () => { await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); it('should work when there is a user to delete', async () => { const { sut, context, mocks } = await setup(await getKyselyDB()); - const user = TestFactory.user({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() }); + const user = TestFactory.user({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() }); await context.createUser(user); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([ + { name: JobName.USER_DELETION, data: { id: user.id } }, + ]); }); it('should skip a recently deleted user', async () => { @@ -150,7 +159,7 @@ describe.concurrent(UserService.name, () => { await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); it('should respect a custom user delete delay', async () => { @@ -166,7 +175,7 @@ describe.concurrent(UserService.name, () => { await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); }); }); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 531f8d56f1..a17ca03e85 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -21,7 +21,6 @@ export const newAssetRepositoryMock = (): Mocked> => { return { generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()), + writeExif: vitest.fn().mockImplementation(() => Promise.resolve()), generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')), decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }), extract: vitest.fn().mockResolvedValue(false), diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 5b228d3afb..0f6d059b6a 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -1,9 +1,8 @@ import { randomUUID } from 'node:crypto'; import { ApiKey, Asset, AuthApiKey, AuthUser, Library, Partner, User, UserAdmin } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; -import { OnThisDayData } from 'src/entities/memory.entity'; import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; -import { ActivityItem, MemoryItem } from 'src/types'; +import { ActivityItem, MemoryItem, OnThisDayData } from 'src/types'; export const newUuid = () => randomUUID() as string; export const newUuids = () => diff --git a/server/test/sql-tools/check-constraint-default-name.stub.ts b/server/test/sql-tools/check-constraint-default-name.stub.ts new file mode 100644 index 0000000000..42ee336b94 --- /dev/null +++ b/server/test/sql-tools/check-constraint-default-name.stub.ts @@ -0,0 +1,41 @@ +import { Check, Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +@Check({ expression: '1=1' }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should create a check constraint with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.CHECK, + name: 'CHK_8d2ecfd49b984941f6b2589799', + tableName: 'table1', + expression: '1=1', + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/check-constraint-override-name.stub.ts b/server/test/sql-tools/check-constraint-override-name.stub.ts new file mode 100644 index 0000000000..89db6044a2 --- /dev/null +++ b/server/test/sql-tools/check-constraint-override-name.stub.ts @@ -0,0 +1,41 @@ +import { Check, Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +@Check({ name: 'CHK_test', expression: '1=1' }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should create a check constraint with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.CHECK, + name: 'CHK_test', + tableName: 'table1', + expression: '1=1', + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-boolean.stub.ts b/server/test/sql-tools/column-default-boolean.stub.ts new file mode 100644 index 0000000000..464a34b26e --- /dev/null +++ b/server/test/sql-tools/column-default-boolean.stub.ts @@ -0,0 +1,33 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'boolean', default: true }) + column1!: boolean; +} + +export const description = 'should register a table with a column with a default value (boolean)'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'boolean', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + default: 'true', + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-date.stub.ts b/server/test/sql-tools/column-default-date.stub.ts new file mode 100644 index 0000000000..72c06b3bd9 --- /dev/null +++ b/server/test/sql-tools/column-default-date.stub.ts @@ -0,0 +1,35 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +const date = new Date(2023, 0, 1); + +@Table() +export class Table1 { + @Column({ type: 'character varying', default: date }) + column1!: string; +} + +export const description = 'should register a table with a column with a default value (date)'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + default: "'2023-01-01T00:00:00.000Z'", + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-function.stub.ts b/server/test/sql-tools/column-default-function.stub.ts new file mode 100644 index 0000000000..ceb03b50f0 --- /dev/null +++ b/server/test/sql-tools/column-default-function.stub.ts @@ -0,0 +1,33 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'character varying', default: () => 'now()' }) + column1!: string; +} + +export const description = 'should register a table with a column with a default function'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + default: 'now()', + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-null.stub.ts b/server/test/sql-tools/column-default-null.stub.ts new file mode 100644 index 0000000000..b4aa83788b --- /dev/null +++ b/server/test/sql-tools/column-default-null.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'character varying', default: null }) + column1!: string; +} + +export const description = 'should register a nullable column from a default of null'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-number.stub.ts b/server/test/sql-tools/column-default-number.stub.ts new file mode 100644 index 0000000000..f3fac229c7 --- /dev/null +++ b/server/test/sql-tools/column-default-number.stub.ts @@ -0,0 +1,33 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'integer', default: 0 }) + column1!: string; +} + +export const description = 'should register a table with a column with a default value (number)'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'integer', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + default: '0', + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-default-string.stub.ts b/server/test/sql-tools/column-default-string.stub.ts new file mode 100644 index 0000000000..36aa584eeb --- /dev/null +++ b/server/test/sql-tools/column-default-string.stub.ts @@ -0,0 +1,33 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'character varying', default: 'foo' }) + column1!: string; +} + +export const description = 'should register a table with a column with a default value (string)'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + default: "'foo'", + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-enum-name.stub.ts b/server/test/sql-tools/column-enum-name.stub.ts new file mode 100644 index 0000000000..9ae1b4310d --- /dev/null +++ b/server/test/sql-tools/column-enum-name.stub.ts @@ -0,0 +1,39 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +enum Test { + Foo = 'foo', + Bar = 'bar', +} + +@Table() +export class Table1 { + @Column({ enum: Test }) + column1!: string; +} + +export const description = 'should use a default enum naming convention'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'enum', + enumName: 'table1_column1_enum', + enumValues: ['foo', 'bar'], + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts new file mode 100644 index 0000000000..d3b5aba112 --- /dev/null +++ b/server/test/sql-tools/column-index-name-default.ts @@ -0,0 +1,41 @@ +import { Column, ColumnIndex, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @ColumnIndex() + @Column() + column1!: string; +} + +export const description = 'should create a column with an index'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_50c4f9905061b1e506d38a2a38', + columnNames: ['column1'], + tableName: 'table1', + unique: false, + synchronize: true, + }, + ], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-inferred-nullable.stub.ts b/server/test/sql-tools/column-inferred-nullable.stub.ts new file mode 100644 index 0000000000..d866b59093 --- /dev/null +++ b/server/test/sql-tools/column-inferred-nullable.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ default: null }) + column1!: string; +} + +export const description = 'should infer nullable from the default value'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-name-default.stub.ts b/server/test/sql-tools/column-name-default.stub.ts new file mode 100644 index 0000000000..3c6df97fe4 --- /dev/null +++ b/server/test/sql-tools/column-name-default.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column() + column1!: string; +} + +export const description = 'should register a table with a column with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-name-override.stub.ts b/server/test/sql-tools/column-name-override.stub.ts new file mode 100644 index 0000000000..b5e86e47d0 --- /dev/null +++ b/server/test/sql-tools/column-name-override.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ name: 'column-1' }) + column1!: string; +} + +export const description = 'should register a table with a column with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column-1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-name-string.stub.ts b/server/test/sql-tools/column-name-string.stub.ts new file mode 100644 index 0000000000..013e74e7da --- /dev/null +++ b/server/test/sql-tools/column-name-string.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column('column-1') + column1!: string; +} + +export const description = 'should register a table with a column with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column-1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-nullable.stub.ts b/server/test/sql-tools/column-nullable.stub.ts new file mode 100644 index 0000000000..2704fb7cf6 --- /dev/null +++ b/server/test/sql-tools/column-nullable.stub.ts @@ -0,0 +1,32 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ nullable: true }) + column1!: string; +} + +export const description = 'should set nullable correctly'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts new file mode 100644 index 0000000000..6446a2069d --- /dev/null +++ b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts @@ -0,0 +1,40 @@ +import { Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'uuid', unique: true }) + id!: string; +} + +export const description = 'should create a unique key constraint with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_b249cc64cf63b8a22557cdc8537', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts new file mode 100644 index 0000000000..fb96ff06b2 --- /dev/null +++ b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts @@ -0,0 +1,40 @@ +import { Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ type: 'uuid', unique: true, uniqueConstraintName: 'UQ_test' }) + id!: string; +} + +export const description = 'should create a unique key constraint with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts new file mode 100644 index 0000000000..b88d834a76 --- /dev/null +++ b/server/test/sql-tools/foreign-key-inferred-type.stub.ts @@ -0,0 +1,73 @@ +import { DatabaseConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @PrimaryColumn({ type: 'uuid' }) + id!: string; +} + +@Table() +export class Table2 { + @ForeignKeyColumn(() => Table1, {}) + parentId!: string; +} + +export const description = 'should infer the column type from the reference column'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: true, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_b249cc64cf63b8a22557cdc8537', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + { + name: 'table2', + columns: [ + { + name: 'parentId', + tableName: 'table2', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_3fcca5cc563abf256fc346e3ff4', + tableName: 'table2', + columnNames: ['parentId'], + referenceColumnNames: ['id'], + referenceTableName: 'table1', + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts new file mode 100644 index 0000000000..8bf2328fc3 --- /dev/null +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -0,0 +1,80 @@ +import { DatabaseConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @PrimaryColumn({ type: 'uuid' }) + id!: string; +} + +@Table() +export class Table2 { + @ForeignKeyColumn(() => Table1, { unique: true }) + parentId!: string; +} + +export const description = 'should create a foreign key constraint with a unique constraint'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: true, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_b249cc64cf63b8a22557cdc8537', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + { + name: 'table2', + columns: [ + { + name: 'parentId', + tableName: 'table2', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.FOREIGN_KEY, + name: 'FK_3fcca5cc563abf256fc346e3ff4', + tableName: 'table2', + columnNames: ['parentId'], + referenceColumnNames: ['id'], + referenceTableName: 'table1', + synchronize: true, + }, + { + type: DatabaseConstraintType.UNIQUE, + name: 'REL_3fcca5cc563abf256fc346e3ff', + tableName: 'table2', + columnNames: ['parentId'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/index-name-default.stub.ts b/server/test/sql-tools/index-name-default.stub.ts new file mode 100644 index 0000000000..ffadfb0b32 --- /dev/null +++ b/server/test/sql-tools/index-name-default.stub.ts @@ -0,0 +1,41 @@ +import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; + +@Table() +@Index({ columns: ['id'] }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should create an index with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_b249cc64cf63b8a22557cdc853', + tableName: 'table1', + unique: false, + columnNames: ['id'], + synchronize: true, + }, + ], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/index-name-override.stub.ts b/server/test/sql-tools/index-name-override.stub.ts new file mode 100644 index 0000000000..f72a0cbeb1 --- /dev/null +++ b/server/test/sql-tools/index-name-override.stub.ts @@ -0,0 +1,41 @@ +import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; + +@Table() +@Index({ name: 'IDX_test', columns: ['id'] }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should create an index with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_test', + tableName: 'table1', + unique: false, + columnNames: ['id'], + synchronize: true, + }, + ], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/index-with-where.stub copy.ts b/server/test/sql-tools/index-with-where.stub copy.ts new file mode 100644 index 0000000000..0d22f4e115 --- /dev/null +++ b/server/test/sql-tools/index-with-where.stub copy.ts @@ -0,0 +1,41 @@ +import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; + +@Table() +@Index({ expression: '"id" IS NOT NULL' }) +export class Table1 { + @Column({ nullable: true }) + column1!: string; +} + +export const description = 'should create an index based off of an expression'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_376788d186160c4faa5aaaef63', + tableName: 'table1', + unique: false, + expression: '"id" IS NOT NULL', + synchronize: true, + }, + ], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/index-with-where.stub.ts b/server/test/sql-tools/index-with-where.stub.ts new file mode 100644 index 0000000000..e59d2ec36b --- /dev/null +++ b/server/test/sql-tools/index-with-where.stub.ts @@ -0,0 +1,42 @@ +import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; + +@Table() +@Index({ columns: ['id'], where: '"id" IS NOT NULL' }) +export class Table1 { + @Column({ nullable: true }) + column1!: string; +} + +export const description = 'should create an index with a where clause'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: true, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_9f4e073964c0395f51f9b39900', + tableName: 'table1', + unique: false, + columnNames: ['id'], + where: '"id" IS NOT NULL', + synchronize: true, + }, + ], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts new file mode 100644 index 0000000000..d4b426b9f1 --- /dev/null +++ b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts @@ -0,0 +1,40 @@ +import { DatabaseConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @PrimaryColumn({ type: 'uuid' }) + id!: string; +} + +export const description = 'should add a primary key constraint to the table with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: true, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_b249cc64cf63b8a22557cdc8537', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts new file mode 100644 index 0000000000..717d9165b3 --- /dev/null +++ b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts @@ -0,0 +1,40 @@ +import { DatabaseConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; + +@Table({ primaryConstraintName: 'PK_test' }) +export class Table1 { + @PrimaryColumn({ type: 'uuid' }) + id!: string; +} + +export const description = 'should add a primary key constraint to the table with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: true, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.PRIMARY_KEY, + name: 'PK_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/table-name-default.stub.ts b/server/test/sql-tools/table-name-default.stub.ts new file mode 100644 index 0000000000..a76a5b6dbb --- /dev/null +++ b/server/test/sql-tools/table-name-default.stub.ts @@ -0,0 +1,19 @@ +import { DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 {} + +export const description = 'should register a table with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/table-name-override.stub.ts b/server/test/sql-tools/table-name-override.stub.ts new file mode 100644 index 0000000000..3290fab6a4 --- /dev/null +++ b/server/test/sql-tools/table-name-override.stub.ts @@ -0,0 +1,19 @@ +import { DatabaseSchema, Table } from 'src/sql-tools'; + +@Table({ name: 'table-1' }) +export class Table1 {} + +export const description = 'should register a table with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table-1', + columns: [], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/table-name-string-option.stub.ts b/server/test/sql-tools/table-name-string-option.stub.ts new file mode 100644 index 0000000000..0c9a045d5b --- /dev/null +++ b/server/test/sql-tools/table-name-string-option.stub.ts @@ -0,0 +1,19 @@ +import { DatabaseSchema, Table } from 'src/sql-tools'; + +@Table('table-1') +export class Table1 {} + +export const description = 'should register a table with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table-1', + columns: [], + indexes: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/unique-constraint-name-default.stub.ts b/server/test/sql-tools/unique-constraint-name-default.stub.ts new file mode 100644 index 0000000000..42fc63bc46 --- /dev/null +++ b/server/test/sql-tools/unique-constraint-name-default.stub.ts @@ -0,0 +1,41 @@ +import { Column, DatabaseConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; + +@Table() +@Unique({ columns: ['id'] }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should add a unique constraint to the table with a default name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_b249cc64cf63b8a22557cdc8537', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/unique-constraint-name-override.stub.ts b/server/test/sql-tools/unique-constraint-name-override.stub.ts new file mode 100644 index 0000000000..e7f6fcf83c --- /dev/null +++ b/server/test/sql-tools/unique-constraint-name-override.stub.ts @@ -0,0 +1,41 @@ +import { Column, DatabaseConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; + +@Table() +@Unique({ name: 'UQ_test', columns: ['id'] }) +export class Table1 { + @Column({ type: 'uuid' }) + id!: string; +} + +export const description = 'should add a unique constraint to the table with a specific name'; +export const schema: DatabaseSchema = { + name: 'public', + tables: [ + { + name: 'table1', + columns: [ + { + name: 'id', + tableName: 'table1', + type: 'uuid', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [], + constraints: [ + { + type: DatabaseConstraintType.UNIQUE, + name: 'UQ_test', + tableName: 'table1', + columnNames: ['id'], + synchronize: true, + }, + ], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/utils.ts b/server/test/utils.ts index 988d4cd97e..4df7904d75 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -170,7 +170,9 @@ export const newTestService = ( const mocks: ServiceMocks = { access: newAccessRepositoryMock(), + // eslint-disable-next-line no-sparse-arrays logger: automock(LoggingRepository, { args: [, configMock], strict: false }), + // eslint-disable-next-line no-sparse-arrays cron: automock(CronRepository, { args: [, loggerMock] }), crypto: newCryptoRepositoryMock(), activity: automock(ActivityRepository), @@ -181,6 +183,7 @@ export const newTestService = ( config: newConfigRepositoryMock(), database: newDatabaseRepositoryMock(), downloadRepository: automock(DownloadRepository, { strict: false }), + // eslint-disable-next-line no-sparse-arrays event: automock(EventRepository, { args: [, , loggerMock], strict: false }), job: newJobRepositoryMock(), apiKey: automock(ApiKeyRepository), @@ -197,6 +200,7 @@ export const newTestService = ( person: automock(PersonRepository, { strict: false }), process: automock(ProcessRepository, { args: [loggerMock] }), search: automock(SearchRepository, { args: [loggerMock], strict: false }), + // eslint-disable-next-line no-sparse-arrays serverInfo: automock(ServerInfoRepository, { args: [, loggerMock], strict: false }), session: automock(SessionRepository), sharedLink: automock(SharedLinkRepository), @@ -205,6 +209,7 @@ export const newTestService = ( sync: automock(SyncRepository), systemMetadata: newSystemMetadataRepositoryMock(), // systemMetadata: automock(SystemMetadataRepository, { strict: false }), + // eslint-disable-next-line no-sparse-arrays tag: automock(TagRepository, { args: [, loggerMock], strict: false }), telemetry: newTelemetryRepositoryMock(), trash: automock(TrashRepository), diff --git a/server/test/vitest.config.mjs b/server/test/vitest.config.mjs index 071e4886f2..d3d1c98f5d 100644 --- a/server/test/vitest.config.mjs +++ b/server/test/vitest.config.mjs @@ -12,12 +12,13 @@ export default defineConfig({ include: ['src/**/*.spec.ts'], coverage: { provider: 'v8', - include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'], + include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**', 'src/sql-tools/**'], exclude: [ 'src/services/*.spec.ts', 'src/services/api.service.ts', 'src/services/microservices.service.ts', 'src/services/index.ts', + 'src/sql-tools/schema-from-database.ts', ], thresholds: { lines: 85, diff --git a/web/eslint.config.mjs b/web/eslint.config.js similarity index 83% rename from web/eslint.config.mjs rename to web/eslint.config.js index f855a99c53..5c24cd1aeb 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.js @@ -1,21 +1,16 @@ -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'; import parser from 'svelte-eslint-parser'; +import typescriptEslint from 'typescript-eslint'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); -export default [ +export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, eslintPluginUnicorn.configs.recommended, js.configs.recommended, @@ -33,16 +28,15 @@ export default [ '**/package-lock.json', '**/yarn.lock', '**/svelte.config.js', - 'eslint.config.mjs', + 'eslint.config.js', 'postcss.config.cjs', 'tailwind.config.js', 'coverage', ], }, - ...compat.extends('plugin:@typescript-eslint/recommended'), + typescriptEslint.configs.recommended, { plugins: { - '@typescript-eslint': typescriptEslint, svelte: eslintPluginSvelte, }, @@ -53,7 +47,7 @@ export default [ NodeJS: true, }, - parser: tsParser, + parser: typescriptEslint.parser, ecmaVersion: 2022, sourceType: 'module', @@ -100,8 +94,8 @@ export default [ sourceType: 'script', parserOptions: { - parser: '@typescript-eslint/parser', + parser: typescriptEslint.parser, }, }, }, -]; +); diff --git a/web/package-lock.json b/web/package-lock.json index 889baca943..0df269cfa0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.130.3", + "version": "1.131.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.130.3", + "version": "1.131.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -27,13 +27,14 @@ "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", + "maplibre-gl": "^5.3.0", "pmtiles": "^4.3.0", "qrcode": "^1.5.4", "socket.io-client": "~4.8.0", "svelte-gestures": "^5.1.3", "svelte-i18n": "^4.0.1", - "svelte-local-storage-store": "^0.6.4", - "svelte-maplibre": "^0.9.13", + "svelte-maplibre": "^1.0.0", + "svelte-persisted-store": "^0.12.0", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -53,8 +54,6 @@ "@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", "autoprefixer": "^10.4.17", "dotenv": "^16.4.7", @@ -75,19 +74,20 @@ "tailwindcss": "^3.4.17", "tslib": "^2.6.2", "typescript": "^5.7.3", + "typescript-eslint": "^8.28.0", "vite": "^6.0.0", "vitest": "^3.0.0" } }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.130.3", + "version": "1.131.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.10", + "@types/node": "^22.13.14", "typescript": "^5.3.3" } }, @@ -846,54 +846,54 @@ "license": "MIT" }, "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.3.tgz", - "integrity": "sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", "license": "MIT", "dependencies": { - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/intl-localematcher": "0.6.0", - "decimal.js": "10", - "tslib": "2" + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/fast-memoize": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", - "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", "license": "MIT", "dependencies": { - "tslib": "2" + "tslib": "^2.8.0" } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.1.tgz", - "integrity": "sha512-o0AhSNaOfKoic0Sn1GkFCK4MxdRsw7mPJ5/rBpIqdvcC7MIuyUSW8WChUEvrK78HhNpYOgqCQbINxCTumJLzZA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.3", - "@formatjs/icu-skeleton-parser": "1.8.13", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.13", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.13.tgz", - "integrity": "sha512-N/LIdTvVc1TpJmMt2jVg0Fr1F7Q1qJPdZSCs19unMskCmVQ/sa0H9L8PWt13vq+gLdLg1+pPsvBLydL1Apahjg==", + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.3", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.0.tgz", - "integrity": "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", "license": "MIT", "dependencies": { - "tslib": "2" + "tslib": "^2.8.0" } }, "node_modules/@humanfs/core": { @@ -1630,7 +1630,8 @@ "node_modules/@mapbox/unitbezier": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", @@ -1649,16 +1650,18 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.1.1.tgz", - "integrity": "sha512-z85ARNPCBI2Cs5cPOS3DSbraTN+ue8zrcYVoSWBuNrD/mA+2SKAJ+hIzI22uN7gac6jBMnCdpPKRxS/V0KSZVQ==", + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.1.0.tgz", + "integrity": "sha512-R6/ihEuC5KRexmKIYkWqUv84Gm+/QwsOUgHyt1yy2XqCdGdLvlBWVWIIeTZWN4NGdwmY6xDzdSGU2R9oBLNg2w==", + "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", + "quickselect": "^3.0.0", "rw": "^1.3.3", - "sort-object": "^3.0.3" + "tinyqueue": "^3.0.0" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", @@ -1666,6 +1669,18 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/@mdi/js": { "version": "7.4.47", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", @@ -2475,9 +2490,10 @@ "license": "MIT" }, "node_modules/@types/geojson": { - "version": "7946.0.14", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" }, "node_modules/@types/geojson-vt": { "version": "3.2.5", @@ -2525,10 +2541,11 @@ } }, "node_modules/@types/luxon": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", - "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", - "dev": true + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz", + "integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.4", @@ -2582,17 +2599,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2612,16 +2629,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "engines": { @@ -2637,14 +2654,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2655,14 +2672,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2679,9 +2696,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "license": "MIT", "engines": { @@ -2693,14 +2710,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2746,16 +2763,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2770,13 +2787,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2801,9 +2818,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", - "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", + "integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2816,7 +2833,7 @@ "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, @@ -2824,8 +2841,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.9", - "vitest": "3.0.9" + "@vitest/browser": "3.1.1", + "vitest": "3.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2852,14 +2869,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2868,13 +2885,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2895,9 +2912,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "dev": true, "license": "MIT", "dependencies": { @@ -2908,13 +2925,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.1.1", "pathe": "^2.0.3" }, "funding": { @@ -2922,13 +2939,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2937,9 +2954,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2950,13 +2967,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -3162,14 +3179,6 @@ "dequal": "^2.0.3" } }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3180,14 +3189,6 @@ "node": ">=12" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3352,23 +3353,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytewise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", - "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", - "dependencies": { - "bytewise-core": "^1.2.2", - "typewise": "^1.0.3" - } - }, - "node_modules/bytewise-core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", - "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", - "dependencies": { - "typewise-core": "^1.2" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4012,7 +3996,8 @@ "node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "peer": true }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -4293,9 +4278,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.3.3.tgz", - "integrity": "sha512-imzGqIgWbfsb/CR14d3k3M8MiVNGet+l9mjPhvo1Rm0Nxi0rNn4/eELqyR8FWlgKBMlGkOp2kshRJm0xpxNfHQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.4.1.tgz", + "integrity": "sha512-wgbRwN/6FampBBiIuuLSmp4QRqmuHuexbuRJwx+kqzsxKOhakU8o8sVgGhsf/bQiZkOmWF/5Mrj2CHmVMwY+YQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4308,7 +4293,7 @@ "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", - "svelte-eslint-parser": "^1.0.1" + "svelte-eslint-parser": "^1.1.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4684,9 +4669,9 @@ } }, "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4701,17 +4686,6 @@ "type": "^2.7.2" } }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fabric": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.1.tgz", @@ -5206,7 +5180,8 @@ "node_modules/geojson-vt": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", + "peer": true }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -5227,14 +5202,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gl-matrix": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", @@ -5296,27 +5263,41 @@ } }, "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" } }, "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "which": "bin/which" + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/globals": { @@ -5604,9 +5585,13 @@ "optional": true }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/inline-style-parser": { "version": "0.2.4", @@ -5623,15 +5608,15 @@ } }, "node_modules/intl-messageformat": { - "version": "10.7.15", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.15.tgz", - "integrity": "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==", + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.3", - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/icu-messageformat-parser": "2.11.1", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" } }, "node_modules/is-binary-path": { @@ -5687,14 +5672,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5735,6 +5712,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -5783,6 +5761,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5976,18 +5955,14 @@ "node_modules/json-stringify-pretty-compact": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", - "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==" + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" }, "node_modules/just-compare": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", "integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==" }, - "node_modules/just-flush": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/just-flush/-/just-flush-2.3.0.tgz", - "integrity": "sha512-fBuxQ1gJ61BurmhwKS5LYTzhkbrT5j/2U7ax+UbLm9aRvCTh2h6AfzLteOckE4KKomqOf0Y3zIG3Xu57sRsKUg==" - }, "node_modules/justified-layout": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz", @@ -6118,9 +6093,9 @@ } }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "license": "MIT", "engines": { "node": ">=12" @@ -6244,9 +6219,10 @@ } }, "node_modules/maplibre-gl": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", - "integrity": "sha512-UF+wI2utIciFXNg6+gYaMe7IGa9fMLzAZM3vdlGilqyWYmuibjcN40yGVgkz2r28//aOLphvtli3TbDEjEqHww==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.0.tgz", + "integrity": "sha512-qru6B6jHlDPR4Q9/P4W1zEPbPofR4wwYbrrjiHKWI7yLtyXmpJ1/G1KaIYDr5uNdFbPZ7uiZAWdqtfdNLmIhGg==", + "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -6255,24 +6231,24 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.1.1", - "@types/geojson": "^7946.0.14", + "@maplibre/maplibre-gl-style-spec": "^23.1.0", + "@types/geojson": "^7946.0.16", "@types/geojson-vt": "3.2.5", "@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__vector-tile": "^1.3.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", - "earcut": "^2.2.4", - "geojson-vt": "^3.2.1", + "earcut": "^3.0.1", + "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", - "global-prefix": "^3.0.0", + "global-prefix": "^4.0.0", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", + "pbf": "^3.3.0", "potpack": "^2.0.0", - "quickselect": "^2.0.0", + "quickselect": "^3.0.0", "supercluster": "^8.0.1", - "tinyqueue": "^2.0.3", + "tinyqueue": "^3.0.0", "vt-pbf": "^3.1.3" }, "engines": { @@ -6283,6 +6259,30 @@ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" } }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/memoizee": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", @@ -6843,9 +6843,10 @@ } }, "node_modules/pbf": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", - "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -7425,7 +7426,8 @@ "node_modules/quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "peer": true }, "node_modules/react-is": { "version": "17.0.2", @@ -7869,20 +7871,6 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -8061,38 +8049,6 @@ "node": ">=10.0.0" } }, - "node_modules/sort-asc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", - "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-desc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", - "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-object": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", - "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", - "dependencies": { - "bytewise": "^1.1.0", - "get-value": "^2.0.2", - "is-extendable": "^0.1.1", - "sort-asc": "^0.2.0", - "sort-desc": "^0.2.0", - "union-value": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8156,40 +8112,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -8197,9 +8119,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", "dev": true, "license": "MIT" }, @@ -8350,9 +8272,9 @@ } }, "node_modules/svelte": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.3.tgz", - "integrity": "sha512-J9rcZ/xVJonAoESqVGHHZhrNdVbrCfkdB41BP6eiwHMoFShD9it3yZXApVYMHdGfCshBsZCKsajwJeBbS/M1zg==", + "version": "5.25.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.5.tgz", + "integrity": "sha512-ULi9rkVWQJyJYZSpy6SIgSTchWadyWG1QYAUx3JAXL2gXrnhdXtoB20KmXGSNdtNyquq3eYd/gkwAkLcL5PGWw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -8459,9 +8381,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.1.tgz", - "integrity": "sha512-JjdEMXOJqy+dxeaElxbN+meTOtVpHfLnq9VGpiTAOLgM0uHO+ogmUsA3IFgx0x3Wl15pqTZWycCikcD7cAQN/g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.1.1.tgz", + "integrity": "sha512-QLVGPIMDettl30qRHXU2VrPvVJKG8GsGstye7n8rFbEiu3gEARksuQg9Xu4GzubNxhGNM8stfBZkhyMbBQmjFA==", "dev": true, "license": "MIT", "dependencies": { @@ -8956,35 +8878,23 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, - "node_modules/svelte-local-storage-store": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz", - "integrity": "sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==", - "engines": { - "node": ">=0.14" - }, - "peerDependencies": { - "svelte": "^3.48.0 || >4.0.0" - } - }, "node_modules/svelte-maplibre": { - "version": "0.9.14", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.14.tgz", - "integrity": "sha512-5HBvibzU/Uf3g8eEz4Hty5XAwoBhW9Tp7NQEvb80U/glR/M1IHyzUKss6XMq8Zbci2wtsASeoPc6dA5R4+0e0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-1.0.0.tgz", + "integrity": "sha512-lw6t0dnsaYzIPECqymSPnODuWGQnVVzEN1F0cPTXOIDcltEMkefDL3E5MnQ5YU4BY+VYedRDFYo7swEjZwpQCA==", "license": "MIT", "dependencies": { "d3-geo": "^3.1.0", "dequal": "^2.0.3", "just-compare": "^2.3.0", - "just-flush": "^2.3.0", - "maplibre-gl": "^4.0.0", + "maplibre-gl": "^4.0.0 || ^5.0.1", "pmtiles": "^3.0.3" }, "peerDependencies": { - "@deck.gl/core": "^8.8.0", - "@deck.gl/layers": "^8.8.0", - "@deck.gl/mapbox": "^8.8.0", - "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" + "@deck.gl/core": "^9", + "@deck.gl/layers": "^9", + "@deck.gl/mapbox": "^9", + "svelte": "^5.0.0" }, "peerDependenciesMeta": { "@deck.gl/core": { @@ -9021,6 +8931,18 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, + "node_modules/svelte-persisted-store": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/svelte-persisted-store/-/svelte-persisted-store-0.12.0.tgz", + "integrity": "sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==", + "license": "MIT", + "engines": { + "node": ">=0.14" + }, + "peerDependencies": { + "svelte": "^3.48.0 || ^4 || ^5" + } + }, "node_modules/svelte-toolbelt": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.0.tgz", @@ -9322,7 +9244,8 @@ "node_modules/tinyqueue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "peer": true }, "node_modules/tinyrainbow": { "version": "2.0.0", @@ -9462,19 +9385,29 @@ "node": ">=14.17" } }, - "node_modules/typewise": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", - "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", + "node_modules/typescript-eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "dev": true, + "license": "MIT", "dependencies": { - "typewise-core": "^1.2.0" + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/typewise-core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", - "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -9500,20 +9433,6 @@ "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", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -9590,9 +9509,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "dev": true, "license": "MIT", "dependencies": { @@ -9675,9 +9594,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "dev": true, "license": "MIT", "dependencies": { @@ -9735,31 +9654,31 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "debug": "^4.4.0", - "expect-type": "^1.1.0", + "expect-type": "^1.2.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "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.9", + "vite-node": "3.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -9775,8 +9694,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/web/package.json b/web/package.json index 8758f98e5f..4ee7c015c8 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,8 @@ { "name": "immich-web", - "version": "1.130.3", + "version": "1.131.3", "license": "GNU Affero General Public License version 3", + "type": "module", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", "build": "vite build", @@ -22,6 +23,35 @@ "test:watch": "vitest dev", "prepare": "svelte-kit sync" }, + "dependencies": { + "@formatjs/icu-messageformat-parser": "^2.9.8", + "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/ui": "^0.17.3", + "@mapbox/mapbox-gl-rtl-text": "0.2.3", + "@mdi/js": "^7.4.47", + "@photo-sphere-viewer/core": "^5.11.5", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", + "@photo-sphere-viewer/resolution-plugin": "^5.11.5", + "@photo-sphere-viewer/settings-plugin": "^5.11.5", + "@photo-sphere-viewer/video-plugin": "^5.11.5", + "@zoom-image/svelte": "^0.3.0", + "dom-to-image": "^2.6.0", + "fabric": "^6.5.4", + "handlebars": "^4.7.8", + "intl-messageformat": "^10.7.11", + "justified-layout": "^4.1.0", + "lodash-es": "^4.17.21", + "luxon": "^3.4.4", + "maplibre-gl": "^5.3.0", + "pmtiles": "^4.3.0", + "qrcode": "^1.5.4", + "socket.io-client": "~4.8.0", + "svelte-gestures": "^5.1.3", + "svelte-i18n": "^4.0.1", + "svelte-maplibre": "^1.0.0", + "svelte-persisted-store": "^0.12.0", + "thumbhash": "^0.1.1" + }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.18.0", @@ -39,8 +69,6 @@ "@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", "autoprefixer": "^10.4.17", "dotenv": "^16.4.7", @@ -61,38 +89,10 @@ "tailwindcss": "^3.4.17", "tslib": "^2.6.2", "typescript": "^5.7.3", + "typescript-eslint": "^8.28.0", "vite": "^6.0.0", "vitest": "^3.0.0" }, - "type": "module", - "dependencies": { - "@formatjs/icu-messageformat-parser": "^2.9.8", - "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.17.3", - "@mapbox/mapbox-gl-rtl-text": "0.2.3", - "@mdi/js": "^7.4.47", - "@photo-sphere-viewer/core": "^5.11.5", - "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", - "@photo-sphere-viewer/resolution-plugin": "^5.11.5", - "@photo-sphere-viewer/settings-plugin": "^5.11.5", - "@photo-sphere-viewer/video-plugin": "^5.11.5", - "@zoom-image/svelte": "^0.3.0", - "dom-to-image": "^2.6.0", - "fabric": "^6.5.4", - "handlebars": "^4.7.8", - "intl-messageformat": "^10.7.11", - "justified-layout": "^4.1.0", - "lodash-es": "^4.17.21", - "luxon": "^3.4.4", - "pmtiles": "^4.3.0", - "qrcode": "^1.5.4", - "socket.io-client": "~4.8.0", - "svelte-gestures": "^5.1.3", - "svelte-i18n": "^4.0.1", - "svelte-local-storage-store": "^0.6.4", - "svelte-maplibre": "^0.9.13", - "thumbhash": "^0.1.1" - }, "volta": { "node": "22.14.0" } diff --git a/web/src/app.css b/web/src/app.css index 4755f19029..2c8d150b4f 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -128,7 +128,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-400 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; + @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-100 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/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte index e1cb6fa4fb..a19d2b75db 100644 --- a/web/src/lib/actions/__test__/focus-trap-test.svelte +++ b/web/src/lib/actions/__test__/focus-trap-test.svelte @@ -3,15 +3,16 @@ interface Props { show: boolean; + active?: boolean; } - let { show = $bindable() }: Props = $props(); + let { show = $bindable(), active = $bindable() }: Props = $props(); {#if show} -
+
text diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts index 6ce5ad6d5b..d92d8e037d 100644 --- a/web/src/lib/actions/__test__/focus-trap.spec.ts +++ b/web/src/lib/actions/__test__/focus-trap.spec.ts @@ -12,6 +12,12 @@ describe('focusTrap action', () => { expect(document.activeElement).toEqual(screen.getByTestId('one')); }); + it('should not set focus if inactive', async () => { + render(FocusTrapTest, { show: true, active: false }); + await tick(); + expect(document.activeElement).toBe(document.body); + }); + it('supports backward focus wrapping', async () => { render(FocusTrapTest, { show: true }); await tick(); diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index 92775546aa..599a97af75 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe } }; - document.addEventListener('mousedown', handleClick, true); + document.addEventListener('mousedown', handleClick, false); node.addEventListener('keydown', handleKey, false); return { destroy() { - document.removeEventListener('mousedown', handleClick, true); + document.removeEventListener('mousedown', handleClick, false); node.removeEventListener('keydown', handleKey, false); }, }; diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts index 7483e76099..1a84f21729 100644 --- a/web/src/lib/actions/focus-trap.ts +++ b/web/src/lib/actions/focus-trap.ts @@ -1,16 +1,34 @@ import { shortcuts } from '$lib/actions/shortcut'; import { tick } from 'svelte'; -const selectors = - 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; +interface Options { + /** + * Set whether the trap is active or not. + */ + active?: boolean; +} -export function focusTrap(container: HTMLElement) { +const selectors = + 'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)'; + +export function focusTrap(container: HTMLElement, options?: Options) { const triggerElement = document.activeElement; - const focusableElement = container.querySelector(selectors); + const withDefaults = (options?: Options) => { + return { + active: options?.active ?? true, + }; + }; - // Use tick() to ensure focus trap works correctly inside - void tick().then(() => focusableElement?.focus()); + const setInitialFocus = () => { + const focusableElement = container.querySelector(selectors); + // Use tick() to ensure focus trap works correctly inside + void tick().then(() => focusableElement?.focus()); + }; + + if (withDefaults(options).active) { + setInitialFocus(); + } const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => { const focusableElements = container.querySelectorAll(selectors); @@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) { shortcut: { key: 'Tab' }, onShortcut: (event) => { const [firstElement, lastElement] = getFocusableElements(); - if (document.activeElement === lastElement) { + if (document.activeElement === lastElement && withDefaults(options).active) { event.preventDefault(); firstElement?.focus(); } @@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) { shortcut: { key: 'Tab', shift: true }, onShortcut: (event) => { const [firstElement, lastElement] = getFocusableElements(); - if (document.activeElement === firstElement) { + if (document.activeElement === firstElement && withDefaults(options).active) { event.preventDefault(); lastElement?.focus(); } @@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) { ]); return { + update(newOptions?: Options) { + options = newOptions; + if (withDefaults(options).active) { + setInitialFocus(); + } + }, destroy() { destroyShortcuts?.(); if (triggerElement instanceof HTMLElement) { diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index 2f2bcbca64..9a66ad9c97 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -40,7 +40,7 @@
-
+
- (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)} - isEdited={config.image.colorspace !== savedConfig.image.colorspace} - {disabled} - /> + + (config.image.fullsize.enabled = isChecked)} + isEdited={config.image.fullsize.enabled !== savedConfig.image.fullsize.enabled} + {disabled} + /> - (config.image.extractEmbedded = !config.image.extractEmbedded)} - isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} - {disabled} - /> +
+ + + + +
+ +
+ (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)} + isEdited={config.image.colorspace !== savedConfig.image.colorspace} + {disabled} + /> +
+ +
+ (config.image.extractEmbedded = !config.image.extractEmbedded)} + isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} + {disabled} + /> +
-
+
onReset({ ...options, configKeys: ['image'] })} onSave={() => onSave({ image: config.image })} diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index ae2b27efac..9b2aa11552 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -48,7 +48,7 @@