From 8f786fd7dd69b9f482273d124d06a9b0027349a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 27 Mar 2025 19:58:49 -0500 Subject: [PATCH 01/56] fix(web): form reactivity (#17183) --- web/src/app.css | 2 +- .../shared-components/settings/setting-input-field.svelte | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) 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/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte index d904089731..9259f59cfb 100644 --- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte +++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte @@ -71,7 +71,9 @@
- + {#if required}
*
{/if} @@ -109,7 +111,7 @@ max={max.toString()} {step} {required} - {value} + bind:value onchange={handleChange} {disabled} {title} @@ -129,7 +131,7 @@ max={max.toString()} {step} {required} - {value} + bind:value onchange={handleChange} {disabled} {title} From 431cf281dab553d539952285e4648154c80f7bce Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:04:31 -0700 Subject: [PATCH 02/56] chore(web): update typescript-eslint (#17093) --- web/{eslint.config.mjs => eslint.config.js} | 20 ++-- web/package-lock.json | 110 ++++++++++++-------- web/package.json | 85 ++++++++------- 3 files changed, 115 insertions(+), 100 deletions(-) rename web/{eslint.config.mjs => eslint.config.js} (83%) 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..f0273a6e79 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -53,8 +53,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,6 +73,7 @@ "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" } @@ -2582,17 +2581,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "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.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2612,16 +2611,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { @@ -2637,14 +2636,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2655,14 +2654,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2679,9 +2678,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { @@ -2693,14 +2692,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2746,16 +2745,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2770,13 +2769,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -9462,6 +9461,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz", + "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.28.0", + "@typescript-eslint/parser": "8.28.0", + "@typescript-eslint/utils": "8.28.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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", diff --git a/web/package.json b/web/package.json index 8758f98e5f..634d62bafc 100644 --- a/web/package.json +++ b/web/package.json @@ -2,6 +2,7 @@ "name": "immich-web", "version": "1.130.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,49 +23,6 @@ "test:watch": "vitest dev", "prepare": "svelte-kit sync" }, - "devDependencies": { - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.18.0", - "@faker-js/faker": "^9.3.0", - "@socket.io/component-emitter": "^3.1.0", - "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.4.4", - "@sveltejs/kit": "^2.15.2", - "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/svelte": "^5.2.6", - "@testing-library/user-event": "^14.5.2", - "@types/dom-to-image": "^2.6.7", - "@types/justified-layout": "^4.1.4", - "@types/lodash-es": "^4.17.12", - "@types/luxon": "^3.4.2", - "@types/qrcode": "^1.5.5", - "@typescript-eslint/eslint-plugin": "^8.20.0", - "@typescript-eslint/parser": "^8.20.0", - "@vitest/coverage-v8": "^3.0.0", - "autoprefixer": "^10.4.17", - "dotenv": "^16.4.7", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.0", - "eslint-plugin-svelte": "^3.0.0", - "eslint-plugin-unicorn": "^57.0.0", - "factory.ts": "^1.4.1", - "globals": "^16.0.0", - "postcss": "^8.5.0", - "prettier": "^3.4.2", - "prettier-plugin-organize-imports": "^4.0.0", - "prettier-plugin-sort-json": "^4.1.1", - "prettier-plugin-svelte": "^3.3.3", - "rollup-plugin-visualizer": "^5.14.0", - "svelte": "^5.25.3", - "svelte-check": "^4.1.5", - "tailwindcss": "^3.4.17", - "tslib": "^2.6.2", - "typescript": "^5.7.3", - "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", @@ -93,6 +51,47 @@ "svelte-maplibre": "^0.9.13", "thumbhash": "^0.1.1" }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.18.0", + "@faker-js/faker": "^9.3.0", + "@socket.io/component-emitter": "^3.1.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/enhanced-img": "^0.4.4", + "@sveltejs/kit": "^2.15.2", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/svelte": "^5.2.6", + "@testing-library/user-event": "^14.5.2", + "@types/dom-to-image": "^2.6.7", + "@types/justified-layout": "^4.1.4", + "@types/lodash-es": "^4.17.12", + "@types/luxon": "^3.4.2", + "@types/qrcode": "^1.5.5", + "@vitest/coverage-v8": "^3.0.0", + "autoprefixer": "^10.4.17", + "dotenv": "^16.4.7", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.0", + "eslint-plugin-svelte": "^3.0.0", + "eslint-plugin-unicorn": "^57.0.0", + "factory.ts": "^1.4.1", + "globals": "^16.0.0", + "postcss": "^8.5.0", + "prettier": "^3.4.2", + "prettier-plugin-organize-imports": "^4.0.0", + "prettier-plugin-sort-json": "^4.1.1", + "prettier-plugin-svelte": "^3.3.3", + "rollup-plugin-visualizer": "^5.14.0", + "svelte": "^5.25.3", + "svelte-check": "^4.1.5", + "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" + }, "volta": { "node": "22.14.0" } From cc3ea32cd244b3cdd49bd9a1ff7c83bbc5ecaf58 Mon Sep 17 00:00:00 2001 From: Joren Guillaume Date: Fri, 28 Mar 2025 10:35:36 +0100 Subject: [PATCH 03/56] docs: update folder support for app in README.md (#17191) Update folder support for app in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3fde5a83286f24edfb40126cac33122410ba299e Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 28 Mar 2025 07:08:54 -0700 Subject: [PATCH 04/56] feat: map globe view, style hot reloading and load lag fixed (#17079) * chore: upgrade svelte-maplibre and enforce runes * feat: maplibre-gl 5, globe view, style hot reloading, fast map markers * fix: remove location-pin class that wasn't being used --------- Co-authored-by: Zack Pollard --- web/package-lock.json | 343 ++++++------------ web/package.json | 3 +- .../shared-components/map/map.svelte | 235 ++++++------ web/svelte.config.js | 3 + 4 files changed, 251 insertions(+), 333 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index f0273a6e79..d7a4c9d85d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -1629,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", @@ -1648,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", @@ -1665,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", @@ -2474,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", @@ -3161,14 +3178,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", @@ -3179,14 +3188,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", @@ -3351,23 +3352,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", @@ -4011,7 +3995,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", @@ -4700,17 +4685,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", @@ -5205,7 +5179,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", @@ -5226,14 +5201,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", @@ -5295,27 +5262,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": { @@ -5603,9 +5584,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", @@ -5686,14 +5671,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", @@ -5734,6 +5711,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" }, @@ -5782,6 +5760,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" } @@ -5975,18 +5954,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", @@ -6243,9 +6218,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", @@ -6254,24 +6230,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": { @@ -6282,6 +6258,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", @@ -6842,9 +6842,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" @@ -7424,7 +7425,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", @@ -7868,20 +7870,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", @@ -8060,38 +8048,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", @@ -8155,40 +8111,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", @@ -8967,23 +8889,22 @@ } }, "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": { @@ -9321,7 +9242,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", @@ -9484,19 +9406,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/typewise": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", - "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", - "dependencies": { - "typewise-core": "^1.2.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", @@ -9522,20 +9431,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", diff --git a/web/package.json b/web/package.json index 634d62bafc..cf24ccc939 100644 --- a/web/package.json +++ b/web/package.json @@ -42,13 +42,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", "thumbhash": "^0.1.1" }, "devDependencies": { diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 5fba0c6787..dc594bae3f 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -16,7 +16,7 @@ import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url'; import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js'; import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson'; - import type { GeoJSONSource, LngLatLike } from 'maplibre-gl'; + import { type GeoJSONSource, GlobeControl, type LngLatLike } from 'maplibre-gl'; import maplibregl from 'maplibre-gl'; import { t } from 'svelte-i18n'; import { @@ -70,7 +70,6 @@ const theme = $derived($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT); const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl); - const style = $derived(fetch(styleUrl).then((response) => response.json())); export function addClipMapMarker(lng: number, lat: number) { if (map) { @@ -143,112 +142,132 @@ country: featurePoint.properties.country, }; }; + + $effect(() => { + map?.setStyle(styleUrl, { + transformStyle: (previousStyle, nextStyle) => { + if (previousStyle) { + // Preserves the custom map markers from the previous style when the theme is switched + // Required until https://github.com/dimfeld/svelte-maplibre/issues/146 is fixed + const customLayers = previousStyle.layers.filter((l) => l.type == 'fill' && l.source == 'geojson'); + const layers = nextStyle.layers.concat(customLayers); + const sources = nextStyle.sources; + + for (const [key, value] of Object.entries(previousStyle.sources || {})) { + if (key.startsWith('geojson')) { + sources[key] = value; + } + } + + return { + ...nextStyle, + sources, + layers, + }; + } + return nextStyle; + }, + }); + }); -{#await style then style} - event.detail.setMaxZoom(18)} - on:load={(event) => event.detail.on('click', handleMapClick)} - bind:map - > - {#snippet children({ map }: { map: maplibregl.Map })} - - - {#if !simplified} - - - - - {/if} - - {#if showSettingsModal !== undefined} - - - (showSettingsModal = true)}> - - - {/if} - - {#if onOpenInMapView} - - - onOpenInMapView()}> - - - - - {/if} - - asFeature(marker)), - }} - id="geojson" - cluster={{ radius: 500, maxZoom: 24 }} - > - handlePromiseError(handleClusterClick(event.detail.feature.properties?.cluster_id, map))} - > - {#snippet children({ feature }: { feature: maplibregl.Feature })} -
- {feature.properties?.point_count} -
- {/snippet} -
- { - if (!popup) { - handleAssetClick(event.detail.feature.properties?.id, map); - } - }} - > - {#snippet children({ feature }: { feature: Feature })} - {#if useLocationPin} - - {:else} - {feature.properties?.city - {/if} - {#if popup} - - {@render popup?.({ marker: asMarker(feature) })} - - {/if} - {/snippet} - -
- {/snippet} -
- -{/await} + }} + bind:map +> + {#snippet children({ map }: { map: maplibregl.Map })} + + + {#if !simplified} + + + + + {/if} + + {#if showSettingsModal !== undefined} + + + (showSettingsModal = true)}> + + + {/if} + + {#if onOpenInMapView} + + + onOpenInMapView()}> + + + + + {/if} + + asFeature(marker)), + }} + id="geojson" + cluster={{ radius: 35, maxZoom: 17 }} + > + handlePromiseError(handleClusterClick(event.feature.properties?.cluster_id, map))} + > + {#snippet children({ feature })} +
+ {feature.properties?.point_count} +
+ {/snippet} +
+ { + if (!popup) { + handleAssetClick(event.feature.properties?.id, map); + } + }} + > + {#snippet children({ feature }: { feature: Feature })} + {#if useLocationPin} + + {:else} + {feature.properties?.city + {/if} + {#if popup} + + {@render popup?.({ marker: asMarker(feature) })} + + {/if} + {/snippet} + +
+ {/snippet} + diff --git a/web/svelte.config.js b/web/svelte.config.js index c5c93e5330..269ba2d923 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -9,6 +9,9 @@ process.env.PUBLIC_IMMICH_PAY_HOST = process.env.PUBLIC_IMMICH_PAY_HOST || 'http /** @type {import('@sveltejs/kit').Config} */ const config = { + compilerOptions: { + runes: true, + }, preprocess: vitePreprocess(), kit: { adapter: adapter({ From 4b4bcd23f4fff26205e5426d6046b1382ae29cc5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 28 Mar 2025 10:40:09 -0400 Subject: [PATCH 05/56] feat: schema diff sql tools (#17116) --- .github/workflows/test.yml | 4 +- server/eslint.config.mjs | 8 + server/package.json | 4 +- server/src/bin/migrations.ts | 112 +++ server/src/db.d.ts | 25 +- server/src/entities/activity.entity.ts | 55 -- server/src/entities/album-user.entity.ts | 16 - server/src/entities/album.entity.ts | 47 -- server/src/entities/api-key.entity.ts | 34 - server/src/entities/asset-audit.entity.ts | 19 - server/src/entities/asset-face.entity.ts | 35 - server/src/entities/asset-files.entity.ts | 29 - .../src/entities/asset-job-status.entity.ts | 16 - server/src/entities/asset.entity.ts | 123 ---- server/src/entities/audit.entity.ts | 24 - server/src/entities/exif.entity.ts | 75 --- server/src/entities/face-search.entity.ts | 9 - server/src/entities/geodata-places.entity.ts | 60 -- server/src/entities/library.entity.ts | 55 -- server/src/entities/memory.entity.ts | 74 -- server/src/entities/move.entity.ts | 15 - .../natural-earth-countries.entity.ts | 30 - server/src/entities/partner-audit.entity.ts | 19 - server/src/entities/partner.entity.ts | 42 -- server/src/entities/person.entity.ts | 43 -- server/src/entities/session.entity.ts | 20 - server/src/entities/shared-link.entity.ts | 43 -- server/src/entities/smart-search.entity.ts | 9 - server/src/entities/stack.entity.ts | 16 - server/src/entities/sync-checkpoint.entity.ts | 28 - server/src/entities/system-metadata.entity.ts | 31 - server/src/entities/tag.entity.ts | 41 -- server/src/entities/user-audit.entity.ts | 14 - server/src/entities/user-metadata.entity.ts | 9 - server/src/entities/user.entity.ts | 54 -- server/src/entities/version-history.entity.ts | 13 - server/src/repositories/config.repository.ts | 4 +- server/src/repositories/person.repository.ts | 3 +- .../system-metadata.repository.ts | 2 +- server/src/repositories/user.repository.ts | 7 +- server/src/services/base.service.ts | 6 +- server/src/services/memory.service.ts | 2 +- server/src/services/person.service.ts | 10 +- server/src/services/storage.service.ts | 3 +- server/src/services/version.service.ts | 2 +- server/src/sql-tools/decorators.ts | 107 +++ server/src/sql-tools/index.ts | 1 + server/src/sql-tools/public_api.ts | 6 + .../src/sql-tools/schema-diff-to-sql.spec.ts | 473 +++++++++++++ server/src/sql-tools/schema-diff-to-sql.ts | 204 ++++++ server/src/sql-tools/schema-diff.spec.ts | 635 ++++++++++++++++++ server/src/sql-tools/schema-diff.ts | 449 +++++++++++++ server/src/sql-tools/schema-from-database.ts | 394 +++++++++++ .../sql-tools/schema-from-decorators.spec.ts | 31 + .../src/sql-tools/schema-from-decorators.ts | 443 ++++++++++++ server/src/sql-tools/types.ts | 363 ++++++++++ server/src/subscribers/audit.subscriber.ts | 43 -- server/src/tables/activity.table.ts | 56 ++ server/src/tables/album-asset.table.ts | 27 + server/src/tables/album-user.table.ts | 29 + server/src/tables/album.table.ts | 51 ++ server/src/tables/api-key.table.ts | 40 ++ server/src/tables/asset-audit.table.ts | 19 + server/src/tables/asset-face.table.ts | 42 ++ server/src/tables/asset-files.table.ts | 40 ++ server/src/tables/asset-job-status.table.ts | 23 + server/src/tables/asset.table.ts | 138 ++++ server/src/tables/audit.table.ts | 24 + server/src/tables/exif.table.ts | 105 +++ server/src/tables/face-search.table.ts | 16 + server/src/tables/geodata-places.table.ts | 73 ++ server/src/tables/index.ts | 70 ++ server/src/tables/library.table.ts | 46 ++ server/src/tables/memory.table.ts | 60 ++ server/src/tables/memory_asset.table.ts | 14 + server/src/tables/move.table.ts | 24 + .../tables/natural-earth-countries.table.ts | 37 + server/src/tables/partner-audit.table.ts | 19 + server/src/tables/partner.table.ts | 32 + server/src/tables/person.table.ts | 54 ++ server/src/tables/session.table.ts | 40 ++ server/src/tables/shared-link-asset.table.ts | 14 + server/src/tables/shared-link.table.ts | 54 ++ server/src/tables/smart-search.table.ts | 16 + server/src/tables/stack.table.ts | 16 + server/src/tables/sync-checkpoint.table.ts | 34 + server/src/tables/system-metadata.table.ts | 12 + server/src/tables/tag-asset.table.ts | 15 + server/src/tables/tag-closure.table.ts | 15 + server/src/tables/tag.table.ts | 41 ++ server/src/tables/user-audit.table.ts | 14 + server/src/tables/user-metadata.table.ts | 16 + server/src/tables/user.table.ts | 73 ++ server/src/tables/version-history.table.ts | 13 + server/src/types.ts | 28 + server/src/utils/database.ts | 28 - server/src/utils/logger.ts | 6 - server/test/factory.ts | 7 +- server/test/fixtures/audit.stub.ts | 14 - server/test/fixtures/user.stub.ts | 5 - server/test/small.factory.ts | 3 +- .../check-constraint-default-name.stub.ts | 41 ++ .../check-constraint-override-name.stub.ts | 41 ++ .../sql-tools/column-default-boolean.stub.ts | 33 + .../sql-tools/column-default-date.stub.ts | 35 + .../sql-tools/column-default-function.stub.ts | 33 + .../sql-tools/column-default-null.stub.ts | 32 + .../sql-tools/column-default-number.stub.ts | 33 + .../sql-tools/column-default-string.stub.ts | 33 + .../test/sql-tools/column-enum-name.stub.ts | 39 ++ .../sql-tools/column-index-name-default.ts | 41 ++ .../column-inferred-nullable.stub.ts | 32 + .../sql-tools/column-name-default.stub.ts | 32 + .../sql-tools/column-name-override.stub.ts | 32 + .../test/sql-tools/column-name-string.stub.ts | 32 + server/test/sql-tools/column-nullable.stub.ts | 32 + ...umn-unique-constraint-name-default.stub.ts | 40 ++ ...mn-unique-constraint-name-override.stub.ts | 40 ++ .../foreign-key-inferred-type.stub.ts | 73 ++ ...foreign-key-with-unique-constraint.stub.ts | 80 +++ .../test/sql-tools/index-name-default.stub.ts | 41 ++ .../sql-tools/index-name-override.stub.ts | 41 ++ .../sql-tools/index-with-where.stub copy.ts | 41 ++ .../test/sql-tools/index-with-where.stub.ts | 42 ++ ...rimary-key-constraint-name-default.stub.ts | 40 ++ ...imary-key-constraint-name-override.stub.ts | 40 ++ .../test/sql-tools/table-name-default.stub.ts | 19 + .../sql-tools/table-name-override.stub.ts | 19 + .../table-name-string-option.stub.ts | 19 + .../unique-constraint-name-default.stub.ts | 41 ++ .../unique-constraint-name-override.stub.ts | 41 ++ server/test/vitest.config.mjs | 3 +- 132 files changed, 5837 insertions(+), 1246 deletions(-) create mode 100644 server/src/bin/migrations.ts delete mode 100644 server/src/entities/activity.entity.ts delete mode 100644 server/src/entities/api-key.entity.ts delete mode 100644 server/src/entities/asset-audit.entity.ts delete mode 100644 server/src/entities/audit.entity.ts delete mode 100644 server/src/entities/library.entity.ts delete mode 100644 server/src/entities/memory.entity.ts delete mode 100644 server/src/entities/partner-audit.entity.ts delete mode 100644 server/src/entities/partner.entity.ts delete mode 100644 server/src/entities/sync-checkpoint.entity.ts delete mode 100644 server/src/entities/system-metadata.entity.ts delete mode 100644 server/src/entities/user-audit.entity.ts delete mode 100644 server/src/entities/version-history.entity.ts create mode 100644 server/src/sql-tools/decorators.ts create mode 100644 server/src/sql-tools/index.ts create mode 100644 server/src/sql-tools/public_api.ts create mode 100644 server/src/sql-tools/schema-diff-to-sql.spec.ts create mode 100644 server/src/sql-tools/schema-diff-to-sql.ts create mode 100644 server/src/sql-tools/schema-diff.spec.ts create mode 100644 server/src/sql-tools/schema-diff.ts create mode 100644 server/src/sql-tools/schema-from-database.ts create mode 100644 server/src/sql-tools/schema-from-decorators.spec.ts create mode 100644 server/src/sql-tools/schema-from-decorators.ts create mode 100644 server/src/sql-tools/types.ts delete mode 100644 server/src/subscribers/audit.subscriber.ts create mode 100644 server/src/tables/activity.table.ts create mode 100644 server/src/tables/album-asset.table.ts create mode 100644 server/src/tables/album-user.table.ts create mode 100644 server/src/tables/album.table.ts create mode 100644 server/src/tables/api-key.table.ts create mode 100644 server/src/tables/asset-audit.table.ts create mode 100644 server/src/tables/asset-face.table.ts create mode 100644 server/src/tables/asset-files.table.ts create mode 100644 server/src/tables/asset-job-status.table.ts create mode 100644 server/src/tables/asset.table.ts create mode 100644 server/src/tables/audit.table.ts create mode 100644 server/src/tables/exif.table.ts create mode 100644 server/src/tables/face-search.table.ts create mode 100644 server/src/tables/geodata-places.table.ts create mode 100644 server/src/tables/index.ts create mode 100644 server/src/tables/library.table.ts create mode 100644 server/src/tables/memory.table.ts create mode 100644 server/src/tables/memory_asset.table.ts create mode 100644 server/src/tables/move.table.ts create mode 100644 server/src/tables/natural-earth-countries.table.ts create mode 100644 server/src/tables/partner-audit.table.ts create mode 100644 server/src/tables/partner.table.ts create mode 100644 server/src/tables/person.table.ts create mode 100644 server/src/tables/session.table.ts create mode 100644 server/src/tables/shared-link-asset.table.ts create mode 100644 server/src/tables/shared-link.table.ts create mode 100644 server/src/tables/smart-search.table.ts create mode 100644 server/src/tables/stack.table.ts create mode 100644 server/src/tables/sync-checkpoint.table.ts create mode 100644 server/src/tables/system-metadata.table.ts create mode 100644 server/src/tables/tag-asset.table.ts create mode 100644 server/src/tables/tag-closure.table.ts create mode 100644 server/src/tables/tag.table.ts create mode 100644 server/src/tables/user-audit.table.ts create mode 100644 server/src/tables/user-metadata.table.ts create mode 100644 server/src/tables/user.table.ts create mode 100644 server/src/tables/version-history.table.ts delete mode 100644 server/test/fixtures/audit.stub.ts create mode 100644 server/test/sql-tools/check-constraint-default-name.stub.ts create mode 100644 server/test/sql-tools/check-constraint-override-name.stub.ts create mode 100644 server/test/sql-tools/column-default-boolean.stub.ts create mode 100644 server/test/sql-tools/column-default-date.stub.ts create mode 100644 server/test/sql-tools/column-default-function.stub.ts create mode 100644 server/test/sql-tools/column-default-null.stub.ts create mode 100644 server/test/sql-tools/column-default-number.stub.ts create mode 100644 server/test/sql-tools/column-default-string.stub.ts create mode 100644 server/test/sql-tools/column-enum-name.stub.ts create mode 100644 server/test/sql-tools/column-index-name-default.ts create mode 100644 server/test/sql-tools/column-inferred-nullable.stub.ts create mode 100644 server/test/sql-tools/column-name-default.stub.ts create mode 100644 server/test/sql-tools/column-name-override.stub.ts create mode 100644 server/test/sql-tools/column-name-string.stub.ts create mode 100644 server/test/sql-tools/column-nullable.stub.ts create mode 100644 server/test/sql-tools/column-unique-constraint-name-default.stub.ts create mode 100644 server/test/sql-tools/column-unique-constraint-name-override.stub.ts create mode 100644 server/test/sql-tools/foreign-key-inferred-type.stub.ts create mode 100644 server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts create mode 100644 server/test/sql-tools/index-name-default.stub.ts create mode 100644 server/test/sql-tools/index-name-override.stub.ts create mode 100644 server/test/sql-tools/index-with-where.stub copy.ts create mode 100644 server/test/sql-tools/index-with-where.stub.ts create mode 100644 server/test/sql-tools/primary-key-constraint-name-default.stub.ts create mode 100644 server/test/sql-tools/primary-key-constraint-name-override.stub.ts create mode 100644 server/test/sql-tools/table-name-default.stub.ts create mode 100644 server/test/sql-tools/table-name-override.stub.ts create mode 100644 server/test/sql-tools/table-name-string-option.stub.ts create mode 100644 server/test/sql-tools/unique-constraint-name-default.stub.ts create mode 100644 server/test/sql-tools/unique-constraint-name-override.stub.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7b6310667..8317640db5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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/server/eslint.config.mjs b/server/eslint.config.mjs index 5fe62b9651..b1e7d409b1 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -77,6 +77,14 @@ export default [ ], }, ], + + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], }, }, ]; diff --git a/server/package.json b/server/package.json index d600fbad9a..f1d5d4c6b8 100644 --- a/server/package.json +++ b/server/package.json @@ -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;'", diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts new file mode 100644 index 0000000000..13a149a1a1 --- /dev/null +++ b/server/src/bin/migrations.ts @@ -0,0 +1,112 @@ +#!/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 { DatabaseTable, schemaDiff, schemaFromDatabase, schemaFromDecorators } from 'src/sql-tools'; +import 'src/tables'; + +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 { filename, code } = asMigration(name, up, down); + const fullPath = `./src/${filename}`; + writeFileSync(fullPath, code); + 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 }; +}; + +const asMigration = (name: string, up: string[], down: string[]) => { + const timestamp = Date.now(); + + const upSql = up.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n'); + const downSql = down.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n'); + return { + filename: `${timestamp}-${name}.ts`, + code: `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} + } +} +`, + }; +}; + +main() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error(error); + console.log('Something went wrong'); + process.exit(1); + }); diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 85aade2c9b..e315a266cf 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/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/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/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/person.repository.ts b/server/src/repositories/person.repository.ts index d5855d3b91..01b45bd94b 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; @@ -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 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..758f99eec1 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/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) diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index f8c995c007..efdff0e480 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/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/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/person.service.ts b/server/src/services/person.service.ts index e297910a95..c6c3ce4e4f 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -451,11 +451,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; 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/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/tables/activity.table.ts b/server/src/tables/activity.table.ts new file mode 100644 index 0000000000..d7bc7a7bc0 --- /dev/null +++ b/server/src/tables/activity.table.ts @@ -0,0 +1,56 @@ +import { + Check, + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + Index, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { AlbumTable } from 'src/tables/album.table'; +import { AssetTable } from 'src/tables/asset.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/album-asset.table.ts b/server/src/tables/album-asset.table.ts new file mode 100644 index 0000000000..7c51ee9ac2 --- /dev/null +++ b/server/src/tables/album-asset.table.ts @@ -0,0 +1,27 @@ +import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AlbumTable } from 'src/tables/album.table'; +import { AssetTable } from 'src/tables/asset.table'; + +@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/tables/album-user.table.ts b/server/src/tables/album-user.table.ts new file mode 100644 index 0000000000..3f9df51723 --- /dev/null +++ b/server/src/tables/album-user.table.ts @@ -0,0 +1,29 @@ +import { AlbumUserRole } from 'src/enum'; +import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; +import { AlbumTable } from 'src/tables/album.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/album.table.ts b/server/src/tables/album.table.ts new file mode 100644 index 0000000000..4f2f7d88f9 --- /dev/null +++ b/server/src/tables/album.table.ts @@ -0,0 +1,51 @@ +import { AssetOrder } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/api-key.table.ts b/server/src/tables/api-key.table.ts new file mode 100644 index 0000000000..dd4100e86f --- /dev/null +++ b/server/src/tables/api-key.table.ts @@ -0,0 +1,40 @@ +import { Permission } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/asset-audit.table.ts b/server/src/tables/asset-audit.table.ts new file mode 100644 index 0000000000..10f7b535bc --- /dev/null +++ b/server/src/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/tables/asset-face.table.ts b/server/src/tables/asset-face.table.ts new file mode 100644 index 0000000000..623df937af --- /dev/null +++ b/server/src/tables/asset-face.table.ts @@ -0,0 +1,42 @@ +import { SourceType } from 'src/enum'; +import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { PersonTable } from 'src/tables/person.table'; + +@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/tables/asset-files.table.ts b/server/src/tables/asset-files.table.ts new file mode 100644 index 0000000000..fb32070751 --- /dev/null +++ b/server/src/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/tables/asset-job-status.table.ts b/server/src/tables/asset-job-status.table.ts new file mode 100644 index 0000000000..d996577ae4 --- /dev/null +++ b/server/src/tables/asset-job-status.table.ts @@ -0,0 +1,23 @@ +import { Column, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; + +@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/tables/asset.table.ts b/server/src/tables/asset.table.ts new file mode 100644 index 0000000000..7e857b8423 --- /dev/null +++ b/server/src/tables/asset.table.ts @@ -0,0 +1,138 @@ +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; +import { AssetStatus, AssetType } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + Index, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { LibraryTable } from 'src/tables/library.table'; +import { StackTable } from 'src/tables/stack.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/audit.table.ts b/server/src/tables/audit.table.ts new file mode 100644 index 0000000000..a05b070ba7 --- /dev/null +++ b/server/src/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/tables/exif.table.ts b/server/src/tables/exif.table.ts new file mode 100644 index 0000000000..e06659d811 --- /dev/null +++ b/server/src/tables/exif.table.ts @@ -0,0 +1,105 @@ +import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; + +@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/tables/face-search.table.ts b/server/src/tables/face-search.table.ts new file mode 100644 index 0000000000..286d09c677 --- /dev/null +++ b/server/src/tables/face-search.table.ts @@ -0,0 +1,16 @@ +import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AssetFaceTable } from 'src/tables/asset-face.table'; + +@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/tables/geodata-places.table.ts b/server/src/tables/geodata-places.table.ts new file mode 100644 index 0000000000..5216a295cb --- /dev/null +++ b/server/src/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/tables/index.ts b/server/src/tables/index.ts new file mode 100644 index 0000000000..8b92b55187 --- /dev/null +++ b/server/src/tables/index.ts @@ -0,0 +1,70 @@ +import { ActivityTable } from 'src/tables/activity.table'; +import { AlbumAssetTable } from 'src/tables/album-asset.table'; +import { AlbumUserTable } from 'src/tables/album-user.table'; +import { AlbumTable } from 'src/tables/album.table'; +import { APIKeyTable } from 'src/tables/api-key.table'; +import { AssetAuditTable } from 'src/tables/asset-audit.table'; +import { AssetFaceTable } from 'src/tables/asset-face.table'; +import { AssetJobStatusTable } from 'src/tables/asset-job-status.table'; +import { AssetTable } from 'src/tables/asset.table'; +import { AuditTable } from 'src/tables/audit.table'; +import { ExifTable } from 'src/tables/exif.table'; +import { FaceSearchTable } from 'src/tables/face-search.table'; +import { GeodataPlacesTable } from 'src/tables/geodata-places.table'; +import { LibraryTable } from 'src/tables/library.table'; +import { MemoryTable } from 'src/tables/memory.table'; +import { MemoryAssetTable } from 'src/tables/memory_asset.table'; +import { MoveTable } from 'src/tables/move.table'; +import { NaturalEarthCountriesTable, NaturalEarthCountriesTempTable } from 'src/tables/natural-earth-countries.table'; +import { PartnerAuditTable } from 'src/tables/partner-audit.table'; +import { PartnerTable } from 'src/tables/partner.table'; +import { PersonTable } from 'src/tables/person.table'; +import { SessionTable } from 'src/tables/session.table'; +import { SharedLinkAssetTable } from 'src/tables/shared-link-asset.table'; +import { SharedLinkTable } from 'src/tables/shared-link.table'; +import { SmartSearchTable } from 'src/tables/smart-search.table'; +import { StackTable } from 'src/tables/stack.table'; +import { SessionSyncCheckpointTable } from 'src/tables/sync-checkpoint.table'; +import { SystemMetadataTable } from 'src/tables/system-metadata.table'; +import { TagAssetTable } from 'src/tables/tag-asset.table'; +import { UserAuditTable } from 'src/tables/user-audit.table'; +import { UserMetadataTable } from 'src/tables/user-metadata.table'; +import { UserTable } from 'src/tables/user.table'; +import { VersionHistoryTable } from 'src/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/tables/library.table.ts b/server/src/tables/library.table.ts new file mode 100644 index 0000000000..9119c517ea --- /dev/null +++ b/server/src/tables/library.table.ts @@ -0,0 +1,46 @@ +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/memory.table.ts b/server/src/tables/memory.table.ts new file mode 100644 index 0000000000..9523e72610 --- /dev/null +++ b/server/src/tables/memory.table.ts @@ -0,0 +1,60 @@ +import { MemoryType } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; +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/tables/memory_asset.table.ts b/server/src/tables/memory_asset.table.ts new file mode 100644 index 0000000000..543c81c597 --- /dev/null +++ b/server/src/tables/memory_asset.table.ts @@ -0,0 +1,14 @@ +import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { MemoryTable } from 'src/tables/memory.table'; + +@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/tables/move.table.ts b/server/src/tables/move.table.ts new file mode 100644 index 0000000000..cdc00efcaf --- /dev/null +++ b/server/src/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/tables/natural-earth-countries.table.ts b/server/src/tables/natural-earth-countries.table.ts new file mode 100644 index 0000000000..5ac5384afc --- /dev/null +++ b/server/src/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/tables/partner-audit.table.ts b/server/src/tables/partner-audit.table.ts new file mode 100644 index 0000000000..77d9f976b1 --- /dev/null +++ b/server/src/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/tables/partner.table.ts b/server/src/tables/partner.table.ts new file mode 100644 index 0000000000..900f5fa834 --- /dev/null +++ b/server/src/tables/partner.table.ts @@ -0,0 +1,32 @@ +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/person.table.ts b/server/src/tables/person.table.ts new file mode 100644 index 0000000000..206e91e68c --- /dev/null +++ b/server/src/tables/person.table.ts @@ -0,0 +1,54 @@ +import { + Check, + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { AssetFaceTable } from 'src/tables/asset-face.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/session.table.ts b/server/src/tables/session.table.ts new file mode 100644 index 0000000000..4b6afef099 --- /dev/null +++ b/server/src/tables/session.table.ts @@ -0,0 +1,40 @@ +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/shared-link-asset.table.ts b/server/src/tables/shared-link-asset.table.ts new file mode 100644 index 0000000000..da6526dfc8 --- /dev/null +++ b/server/src/tables/shared-link-asset.table.ts @@ -0,0 +1,14 @@ +import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { SharedLinkTable } from 'src/tables/shared-link.table'; + +@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/tables/shared-link.table.ts b/server/src/tables/shared-link.table.ts new file mode 100644 index 0000000000..3a41f5a8f5 --- /dev/null +++ b/server/src/tables/shared-link.table.ts @@ -0,0 +1,54 @@ +import { SharedLinkType } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + Unique, +} from 'src/sql-tools'; +import { AlbumTable } from 'src/tables/album.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/smart-search.table.ts b/server/src/tables/smart-search.table.ts new file mode 100644 index 0000000000..8647756550 --- /dev/null +++ b/server/src/tables/smart-search.table.ts @@ -0,0 +1,16 @@ +import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; + +@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/tables/stack.table.ts b/server/src/tables/stack.table.ts new file mode 100644 index 0000000000..fc711233a4 --- /dev/null +++ b/server/src/tables/stack.table.ts @@ -0,0 +1,16 @@ +import { ForeignKeyColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/sync-checkpoint.table.ts b/server/src/tables/sync-checkpoint.table.ts new file mode 100644 index 0000000000..3fbffccb6c --- /dev/null +++ b/server/src/tables/sync-checkpoint.table.ts @@ -0,0 +1,34 @@ +import { SyncEntityType } from 'src/enum'; +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryColumn, + Table, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { SessionTable } from 'src/tables/session.table'; + +@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/tables/system-metadata.table.ts b/server/src/tables/system-metadata.table.ts new file mode 100644 index 0000000000..8657768db6 --- /dev/null +++ b/server/src/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/tables/tag-asset.table.ts b/server/src/tables/tag-asset.table.ts new file mode 100644 index 0000000000..6080c432b5 --- /dev/null +++ b/server/src/tables/tag-asset.table.ts @@ -0,0 +1,15 @@ +import { ColumnIndex, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; +import { AssetTable } from 'src/tables/asset.table'; +import { TagTable } from 'src/tables/tag.table'; + +@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/tables/tag-closure.table.ts b/server/src/tables/tag-closure.table.ts new file mode 100644 index 0000000000..a661904741 --- /dev/null +++ b/server/src/tables/tag-closure.table.ts @@ -0,0 +1,15 @@ +import { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { TagTable } from 'src/tables/tag.table'; + +@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/tables/tag.table.ts b/server/src/tables/tag.table.ts new file mode 100644 index 0000000000..5b74075647 --- /dev/null +++ b/server/src/tables/tag.table.ts @@ -0,0 +1,41 @@ +import { + Column, + ColumnIndex, + CreateDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + Unique, + UpdateDateColumn, + UpdateIdColumn, +} from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/user-audit.table.ts b/server/src/tables/user-audit.table.ts new file mode 100644 index 0000000000..e3f117381c --- /dev/null +++ b/server/src/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/tables/user-metadata.table.ts b/server/src/tables/user-metadata.table.ts new file mode 100644 index 0000000000..2f83287b6c --- /dev/null +++ b/server/src/tables/user-metadata.table.ts @@ -0,0 +1,16 @@ +import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; +import { UserMetadataKey } from 'src/enum'; +import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { UserTable } from 'src/tables/user.table'; + +@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/tables/user.table.ts b/server/src/tables/user.table.ts new file mode 100644 index 0000000000..5bd9cd94c6 --- /dev/null +++ b/server/src/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/tables/version-history.table.ts b/server/src/tables/version-history.table.ts new file mode 100644 index 0000000000..18805a2de3 --- /dev/null +++ b/server/src/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/types.ts b/server/src/types.ts index 1c0a61b259..6a3860830c 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'; @@ -454,3 +458,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/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/test/factory.ts b/server/test/factory.ts index 69160aa8a4..0becc705bc 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/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, 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/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/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, From c8331f111f6dcb43719c772e3c70b3657ee03410 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:54:31 +0530 Subject: [PATCH 06/56] fix(mobile): prefer remote orientation (#17177) * fix(mobile): prefer remote orientation * pr feedback --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/services/asset.service.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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; From d2bcf5d7166cf287258d6df40f557dbe9ff824bc Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 28 Mar 2025 16:32:25 +0100 Subject: [PATCH 07/56] fix(mobile): pause background video play (#17032) * fix(mobile): prevent background video playback * fix: logic for tracking app state * chore: move lifecycle handler in separate file * chore: replace useState with useRef * chore: useOnAppLifecycleStateChange * fix: removed print statement --- .../pages/common/native_video_viewer.page.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 From 6419ac74afc6932522ae21ee09df557d9017d487 Mon Sep 17 00:00:00 2001 From: Saschl <19493808+Saschl@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:34:19 +0100 Subject: [PATCH 08/56] fix: update renderlist after asset deleted (#16786) --- mobile/lib/pages/common/gallery_viewer.page.dart | 4 ---- mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) 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/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; } From 9f0dbfc150e14a56f5f549cbe6e290a8aa2b2bad Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:40:57 -0700 Subject: [PATCH 09/56] chore(web): update to newer persisted store package name (#17094) --- web/package-lock.json | 25 +++++++++++++------------ web/package.json | 2 +- web/src/lib/stores/preferences.store.ts | 2 +- web/src/lib/stores/search.store.ts | 2 +- web/src/lib/stores/slideshow.store.ts | 2 +- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index d7a4c9d85d..0557c43e6d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -33,8 +33,8 @@ "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": "^1.0.0", + "svelte-persisted-store": "^0.12.0", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -8877,17 +8877,6 @@ "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": "1.0.0", "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-1.0.0.tgz", @@ -8941,6 +8930,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", diff --git a/web/package.json b/web/package.json index cf24ccc939..0656418aff 100644 --- a/web/package.json +++ b/web/package.json @@ -48,8 +48,8 @@ "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": "^1.0.0", + "svelte-persisted-store": "^0.12.0", "thumbhash": "^0.1.1" }, "devDependencies": { diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 818800755c..5a7d21711e 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -1,7 +1,7 @@ import { browser } from '$app/environment'; import { Theme, defaultLang } from '$lib/constants'; import { getPreferredLocale } from '$lib/utils/i18n'; -import { persisted } from 'svelte-local-storage-store'; +import { persisted } from 'svelte-persisted-store'; import { get } from 'svelte/store'; export interface ThemeSetting { diff --git a/web/src/lib/stores/search.store.ts b/web/src/lib/stores/search.store.ts index 41fd287f4c..f8716ed58b 100644 --- a/web/src/lib/stores/search.store.ts +++ b/web/src/lib/stores/search.store.ts @@ -1,4 +1,4 @@ -import { persisted } from 'svelte-local-storage-store'; +import { persisted } from 'svelte-persisted-store'; import { writable } from 'svelte/store'; export const savedSearchTerms = persisted('search-terms', [], {}); diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index 3be96fda3e..5bfcd099cb 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -1,4 +1,4 @@ -import { persisted } from 'svelte-local-storage-store'; +import { persisted } from 'svelte-persisted-store'; import { writable } from 'svelte/store'; export enum SlideshowState { From 6fa0cb534a1d6434159705c105b0447633eca20b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:51:01 +0100 Subject: [PATCH 10/56] fix(deps): update dependency @opentelemetry/context-async-hooks to v2 (#17031) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 22 +++++----------------- server/package.json | 2 +- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 1978d15722..8e06a8bf66 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -19,7 +19,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", @@ -3220,12 +3220,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 +4452,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", diff --git a/server/package.json b/server/package.json index f1d5d4c6b8..2c5959b7af 100644 --- a/server/package.json +++ b/server/package.json @@ -45,7 +45,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", From 55a3c30664464a72e2afc27fedc66e83869e7218 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 29 Mar 2025 09:26:24 -0400 Subject: [PATCH 11/56] feat: kysely migrations (#17198) --- server/src/bin/migrations.ts | 43 ++++++++--- server/src/db.d.ts | 2 +- .../src/repositories/database.repository.ts | 44 ++++++++++- server/src/repositories/user.repository.ts | 2 +- .../src/{ => schema}/tables/activity.table.ts | 6 +- .../{ => schema}/tables/album-asset.table.ts | 4 +- .../{ => schema}/tables/album-user.table.ts | 4 +- server/src/{ => schema}/tables/album.table.ts | 4 +- .../src/{ => schema}/tables/api-key.table.ts | 2 +- .../{ => schema}/tables/asset-audit.table.ts | 0 .../{ => schema}/tables/asset-face.table.ts | 4 +- .../{ => schema}/tables/asset-files.table.ts | 0 .../tables/asset-job-status.table.ts | 2 +- server/src/{ => schema}/tables/asset.table.ts | 6 +- server/src/{ => schema}/tables/audit.table.ts | 0 server/src/{ => schema}/tables/exif.table.ts | 2 +- .../{ => schema}/tables/face-search.table.ts | 2 +- .../tables/geodata-places.table.ts | 0 server/src/schema/tables/index.ts | 73 ++++++++++++++++++ .../src/{ => schema}/tables/library.table.ts | 2 +- .../src/{ => schema}/tables/memory.table.ts | 2 +- .../{ => schema}/tables/memory_asset.table.ts | 4 +- server/src/{ => schema}/tables/move.table.ts | 0 .../tables/natural-earth-countries.table.ts | 0 .../tables/partner-audit.table.ts | 0 .../src/{ => schema}/tables/partner.table.ts | 2 +- .../src/{ => schema}/tables/person.table.ts | 4 +- .../src/{ => schema}/tables/session.table.ts | 2 +- .../tables/shared-link-asset.table.ts | 4 +- .../{ => schema}/tables/shared-link.table.ts | 4 +- .../{ => schema}/tables/smart-search.table.ts | 2 +- server/src/{ => schema}/tables/stack.table.ts | 4 +- .../tables/sync-checkpoint.table.ts | 2 +- .../tables/system-metadata.table.ts | 0 .../{ => schema}/tables/tag-asset.table.ts | 4 +- .../{ => schema}/tables/tag-closure.table.ts | 2 +- server/src/{ => schema}/tables/tag.table.ts | 2 +- .../{ => schema}/tables/user-audit.table.ts | 0 .../tables/user-metadata.table.ts | 2 +- server/src/{ => schema}/tables/user.table.ts | 0 .../tables/version-history.table.ts | 0 server/src/services/base.service.ts | 2 +- server/src/tables/index.ts | 70 ----------------- server/test/factory.ts | 2 +- server/test/medium/globalSetup.ts | 77 ++++++++++++++++++- 45 files changed, 267 insertions(+), 126 deletions(-) rename server/src/{ => schema}/tables/activity.table.ts (87%) rename server/src/{ => schema}/tables/album-asset.table.ts (83%) rename server/src/{ => schema}/tables/album-user.table.ts (88%) rename server/src/{ => schema}/tables/album.table.ts (90%) rename server/src/{ => schema}/tables/api-key.table.ts (92%) rename server/src/{ => schema}/tables/asset-audit.table.ts (100%) rename server/src/{ => schema}/tables/asset-face.table.ts (90%) rename server/src/{ => schema}/tables/asset-files.table.ts (100%) rename server/src/{ => schema}/tables/asset-job-status.table.ts (92%) rename server/src/{ => schema}/tables/asset.table.ts (95%) rename server/src/{ => schema}/tables/audit.table.ts (100%) rename server/src/{ => schema}/tables/exif.table.ts (98%) rename server/src/{ => schema}/tables/face-search.table.ts (87%) rename server/src/{ => schema}/tables/geodata-places.table.ts (100%) create mode 100644 server/src/schema/tables/index.ts rename server/src/{ => schema}/tables/library.table.ts (93%) rename server/src/{ => schema}/tables/memory.table.ts (95%) rename server/src/{ => schema}/tables/memory_asset.table.ts (77%) rename server/src/{ => schema}/tables/move.table.ts (100%) rename server/src/{ => schema}/tables/natural-earth-countries.table.ts (100%) rename server/src/{ => schema}/tables/partner-audit.table.ts (100%) rename server/src/{ => schema}/tables/partner.table.ts (91%) rename server/src/{ => schema}/tables/person.table.ts (90%) rename server/src/{ => schema}/tables/session.table.ts (92%) rename server/src/{ => schema}/tables/shared-link-asset.table.ts (76%) rename server/src/{ => schema}/tables/shared-link.table.ts (91%) rename server/src/{ => schema}/tables/smart-search.table.ts (89%) rename server/src/{ => schema}/tables/stack.table.ts (79%) rename server/src/{ => schema}/tables/sync-checkpoint.table.ts (91%) rename server/src/{ => schema}/tables/system-metadata.table.ts (100%) rename server/src/{ => schema}/tables/tag-asset.table.ts (80%) rename server/src/{ => schema}/tables/tag-closure.table.ts (88%) rename server/src/{ => schema}/tables/tag.table.ts (93%) rename server/src/{ => schema}/tables/user-audit.table.ts (100%) rename server/src/{ => schema}/tables/user-metadata.table.ts (90%) rename server/src/{ => schema}/tables/user.table.ts (100%) rename server/src/{ => schema}/tables/version-history.table.ts (100%) delete mode 100644 server/src/tables/index.ts diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 13a149a1a1..b553ff7fa7 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -4,8 +4,8 @@ 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'; -import 'src/tables'; const main = async () => { const command = process.argv[2]; @@ -54,9 +54,10 @@ const generate = async (name: string) => { }; const create = (name: string, up: string[], down: string[]) => { - const { filename, code } = asMigration(name, up, down); + const timestamp = Date.now(); + const filename = `${timestamp}-${name}.ts`; const fullPath = `./src/${filename}`; - writeFileSync(fullPath, code); + writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down })); console.log(`Wrote ${fullPath}`); }; @@ -79,14 +80,21 @@ const compare = async () => { return { up, down }; }; -const asMigration = (name: string, up: string[], down: string[]) => { - const timestamp = Date.now(); +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 { - filename: `${timestamp}-${name}.ts`, - code: `import { MigrationInterface, QueryRunner } from 'typeorm'; + + return `import { MigrationInterface, QueryRunner } from 'typeorm'; export class ${name}${timestamp} implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -97,8 +105,23 @@ ${upSql} ${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() diff --git a/server/src/db.d.ts b/server/src/db.d.ts index e315a266cf..ca6d1813e4 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -5,7 +5,7 @@ import type { ColumnType } from 'kysely'; import { AssetType, MemoryType, Permission, SyncEntityType } from 'src/enum'; -import { UserTable } from 'src/tables/user.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { OnThisDayData } from 'src/types'; export type ArrayType = ArrayTypeImpl extends (infer U)[] ? U[] : ArrayTypeImpl; diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index c4aeb74028..917a89b47e 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,7 +1,9 @@ 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 { mkdir, 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 +202,49 @@ 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 + await mkdir(migrationFolder, { recursive: true }); + + 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/user.repository.ts b/server/src/repositories/user.repository.ts index 758f99eec1..c619063d04 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -8,7 +8,7 @@ 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/tables/user.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { asUuid } from 'src/utils/database'; type Upsert = Insertable; diff --git a/server/src/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts similarity index 87% rename from server/src/tables/activity.table.ts rename to server/src/schema/tables/activity.table.ts index d7bc7a7bc0..87597838c7 100644 --- a/server/src/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -1,3 +1,6 @@ +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, @@ -10,9 +13,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { AlbumTable } from 'src/tables/album.table'; -import { AssetTable } from 'src/tables/asset.table'; -import { UserTable } from 'src/tables/user.table'; @Table('activity') @Index({ diff --git a/server/src/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts similarity index 83% rename from server/src/tables/album-asset.table.ts rename to server/src/schema/tables/album-asset.table.ts index 7c51ee9ac2..ccd7fda5fd 100644 --- a/server/src/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,6 +1,6 @@ +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'; -import { AlbumTable } from 'src/tables/album.table'; -import { AssetTable } from 'src/tables/asset.table'; @Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' }) export class AlbumAssetTable { diff --git a/server/src/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts similarity index 88% rename from server/src/tables/album-user.table.ts rename to server/src/schema/tables/album-user.table.ts index 3f9df51723..8bd05df2ee 100644 --- a/server/src/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -1,7 +1,7 @@ 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'; -import { AlbumTable } from 'src/tables/album.table'; -import { UserTable } from 'src/tables/user.table'; @Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' }) // Pre-existing indices from original album <--> user ManyToMany mapping diff --git a/server/src/tables/album.table.ts b/server/src/schema/tables/album.table.ts similarity index 90% rename from server/src/tables/album.table.ts rename to server/src/schema/tables/album.table.ts index 4f2f7d88f9..cf2f2e1cb4 100644 --- a/server/src/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -1,4 +1,6 @@ import { AssetOrder } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -10,8 +12,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; -import { UserTable } from 'src/tables/user.table'; @Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' }) export class AlbumTable { diff --git a/server/src/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts similarity index 92% rename from server/src/tables/api-key.table.ts rename to server/src/schema/tables/api-key.table.ts index dd4100e86f..42b98ab957 100644 --- a/server/src/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -1,4 +1,5 @@ import { Permission } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -9,7 +10,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; @Table('api_keys') export class APIKeyTable { diff --git a/server/src/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts similarity index 100% rename from server/src/tables/asset-audit.table.ts rename to server/src/schema/tables/asset-audit.table.ts diff --git a/server/src/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts similarity index 90% rename from server/src/tables/asset-face.table.ts rename to server/src/schema/tables/asset-face.table.ts index 623df937af..56f22cf9a7 100644 --- a/server/src/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -1,7 +1,7 @@ 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'; -import { AssetTable } from 'src/tables/asset.table'; -import { PersonTable } from 'src/tables/person.table'; @Table({ name: 'asset_faces' }) @Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) diff --git a/server/src/tables/asset-files.table.ts b/server/src/schema/tables/asset-files.table.ts similarity index 100% rename from server/src/tables/asset-files.table.ts rename to server/src/schema/tables/asset-files.table.ts diff --git a/server/src/tables/asset-job-status.table.ts b/server/src/schema/tables/asset-job-status.table.ts similarity index 92% rename from server/src/tables/asset-job-status.table.ts rename to server/src/schema/tables/asset-job-status.table.ts index d996577ae4..669ea0a20d 100644 --- a/server/src/tables/asset-job-status.table.ts +++ b/server/src/schema/tables/asset-job-status.table.ts @@ -1,5 +1,5 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, ForeignKeyColumn, Table } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; @Table('asset_job_status') export class AssetJobStatusTable { diff --git a/server/src/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts similarity index 95% rename from server/src/tables/asset.table.ts rename to server/src/schema/tables/asset.table.ts index 7e857b8423..bd79d48149 100644 --- a/server/src/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -1,5 +1,8 @@ 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, @@ -12,9 +15,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { LibraryTable } from 'src/tables/library.table'; -import { StackTable } from 'src/tables/stack.table'; -import { UserTable } from 'src/tables/user.table'; @Table('assets') // Checksums must be unique per user and library diff --git a/server/src/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts similarity index 100% rename from server/src/tables/audit.table.ts rename to server/src/schema/tables/audit.table.ts diff --git a/server/src/tables/exif.table.ts b/server/src/schema/tables/exif.table.ts similarity index 98% rename from server/src/tables/exif.table.ts rename to server/src/schema/tables/exif.table.ts index e06659d811..8eddafecc2 100644 --- a/server/src/tables/exif.table.ts +++ b/server/src/schema/tables/exif.table.ts @@ -1,5 +1,5 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; @Table('exif') export class ExifTable { diff --git a/server/src/tables/face-search.table.ts b/server/src/schema/tables/face-search.table.ts similarity index 87% rename from server/src/tables/face-search.table.ts rename to server/src/schema/tables/face-search.table.ts index 286d09c677..d4da6a69ba 100644 --- a/server/src/tables/face-search.table.ts +++ b/server/src/schema/tables/face-search.table.ts @@ -1,5 +1,5 @@ +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; -import { AssetFaceTable } from 'src/tables/asset-face.table'; @Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' }) export class FaceSearchTable { diff --git a/server/src/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts similarity index 100% rename from server/src/tables/geodata-places.table.ts rename to server/src/schema/tables/geodata-places.table.ts 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/tables/library.table.ts b/server/src/schema/tables/library.table.ts similarity index 93% rename from server/src/tables/library.table.ts rename to server/src/schema/tables/library.table.ts index 9119c517ea..ff0bfd64f7 100644 --- a/server/src/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -1,3 +1,4 @@ +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -9,7 +10,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; @Table('libraries') export class LibraryTable { diff --git a/server/src/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts similarity index 95% rename from server/src/tables/memory.table.ts rename to server/src/schema/tables/memory.table.ts index 9523e72610..91a0412649 100644 --- a/server/src/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -1,4 +1,5 @@ import { MemoryType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -10,7 +11,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; import { MemoryData } from 'src/types'; @Table('memories') diff --git a/server/src/tables/memory_asset.table.ts b/server/src/schema/tables/memory_asset.table.ts similarity index 77% rename from server/src/tables/memory_asset.table.ts rename to server/src/schema/tables/memory_asset.table.ts index 543c81c597..08cdcea442 100644 --- a/server/src/tables/memory_asset.table.ts +++ b/server/src/schema/tables/memory_asset.table.ts @@ -1,6 +1,6 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { MemoryTable } from 'src/schema/tables/memory.table'; import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; -import { MemoryTable } from 'src/tables/memory.table'; @Table('memories_assets_assets') export class MemoryAssetTable { diff --git a/server/src/tables/move.table.ts b/server/src/schema/tables/move.table.ts similarity index 100% rename from server/src/tables/move.table.ts rename to server/src/schema/tables/move.table.ts diff --git a/server/src/tables/natural-earth-countries.table.ts b/server/src/schema/tables/natural-earth-countries.table.ts similarity index 100% rename from server/src/tables/natural-earth-countries.table.ts rename to server/src/schema/tables/natural-earth-countries.table.ts diff --git a/server/src/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts similarity index 100% rename from server/src/tables/partner-audit.table.ts rename to server/src/schema/tables/partner-audit.table.ts diff --git a/server/src/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts similarity index 91% rename from server/src/tables/partner.table.ts rename to server/src/schema/tables/partner.table.ts index 900f5fa834..6406b48277 100644 --- a/server/src/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,3 +1,4 @@ +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -7,7 +8,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; @Table('partners') export class PartnerTable { diff --git a/server/src/tables/person.table.ts b/server/src/schema/tables/person.table.ts similarity index 90% rename from server/src/tables/person.table.ts rename to server/src/schema/tables/person.table.ts index 206e91e68c..91a05d8d76 100644 --- a/server/src/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -1,3 +1,5 @@ +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, @@ -9,8 +11,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { AssetFaceTable } from 'src/tables/asset-face.table'; -import { UserTable } from 'src/tables/user.table'; @Table('person') @Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` }) diff --git a/server/src/tables/session.table.ts b/server/src/schema/tables/session.table.ts similarity index 92% rename from server/src/tables/session.table.ts rename to server/src/schema/tables/session.table.ts index 4b6afef099..287f13de7f 100644 --- a/server/src/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -1,3 +1,4 @@ +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -8,7 +9,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; @Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' }) export class SessionTable { diff --git a/server/src/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts similarity index 76% rename from server/src/tables/shared-link-asset.table.ts rename to server/src/schema/tables/shared-link-asset.table.ts index da6526dfc8..1eb294c1e8 100644 --- a/server/src/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -1,6 +1,6 @@ +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'; -import { AssetTable } from 'src/tables/asset.table'; -import { SharedLinkTable } from 'src/tables/shared-link.table'; @Table('shared_link__asset') export class SharedLinkAssetTable { diff --git a/server/src/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts similarity index 91% rename from server/src/tables/shared-link.table.ts rename to server/src/schema/tables/shared-link.table.ts index 3a41f5a8f5..4372a5760a 100644 --- a/server/src/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -1,4 +1,6 @@ import { SharedLinkType } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -8,8 +10,6 @@ import { Table, Unique, } from 'src/sql-tools'; -import { AlbumTable } from 'src/tables/album.table'; -import { UserTable } from 'src/tables/user.table'; @Table('shared_links') @Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) diff --git a/server/src/tables/smart-search.table.ts b/server/src/schema/tables/smart-search.table.ts similarity index 89% rename from server/src/tables/smart-search.table.ts rename to server/src/schema/tables/smart-search.table.ts index 8647756550..a71eb9ae99 100644 --- a/server/src/tables/smart-search.table.ts +++ b/server/src/schema/tables/smart-search.table.ts @@ -1,5 +1,5 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; @Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' }) export class SmartSearchTable { diff --git a/server/src/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts similarity index 79% rename from server/src/tables/stack.table.ts rename to server/src/schema/tables/stack.table.ts index fc711233a4..ea58ccb425 100644 --- a/server/src/tables/stack.table.ts +++ b/server/src/schema/tables/stack.table.ts @@ -1,6 +1,6 @@ +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { ForeignKeyColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; -import { AssetTable } from 'src/tables/asset.table'; -import { UserTable } from 'src/tables/user.table'; @Table('asset_stack') export class StackTable { diff --git a/server/src/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts similarity index 91% rename from server/src/tables/sync-checkpoint.table.ts rename to server/src/schema/tables/sync-checkpoint.table.ts index 3fbffccb6c..190cd81ffe 100644 --- a/server/src/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -1,4 +1,5 @@ import { SyncEntityType } from 'src/enum'; +import { SessionTable } from 'src/schema/tables/session.table'; import { Column, ColumnIndex, @@ -9,7 +10,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { SessionTable } from 'src/tables/session.table'; @Table('session_sync_checkpoints') export class SessionSyncCheckpointTable { diff --git a/server/src/tables/system-metadata.table.ts b/server/src/schema/tables/system-metadata.table.ts similarity index 100% rename from server/src/tables/system-metadata.table.ts rename to server/src/schema/tables/system-metadata.table.ts diff --git a/server/src/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts similarity index 80% rename from server/src/tables/tag-asset.table.ts rename to server/src/schema/tables/tag-asset.table.ts index 6080c432b5..5f24799cec 100644 --- a/server/src/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -1,6 +1,6 @@ +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'; -import { AssetTable } from 'src/tables/asset.table'; -import { TagTable } from 'src/tables/tag.table'; @Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] }) @Table('tag_asset') diff --git a/server/src/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts similarity index 88% rename from server/src/tables/tag-closure.table.ts rename to server/src/schema/tables/tag-closure.table.ts index a661904741..079dd4dcc5 100644 --- a/server/src/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,5 +1,5 @@ +import { TagTable } from 'src/schema/tables/tag.table'; import { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; -import { TagTable } from 'src/tables/tag.table'; @Table('tags_closure') export class TagClosureTable { diff --git a/server/src/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts similarity index 93% rename from server/src/tables/tag.table.ts rename to server/src/schema/tables/tag.table.ts index 5b74075647..1c6b8cb205 100644 --- a/server/src/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -1,3 +1,4 @@ +import { UserTable } from 'src/schema/tables/user.table'; import { Column, ColumnIndex, @@ -9,7 +10,6 @@ import { UpdateDateColumn, UpdateIdColumn, } from 'src/sql-tools'; -import { UserTable } from 'src/tables/user.table'; @Table('tags') @Unique({ columns: ['userId', 'value'] }) diff --git a/server/src/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts similarity index 100% rename from server/src/tables/user-audit.table.ts rename to server/src/schema/tables/user-audit.table.ts diff --git a/server/src/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts similarity index 90% rename from server/src/tables/user-metadata.table.ts rename to server/src/schema/tables/user-metadata.table.ts index 2f83287b6c..e71b3bf9f9 100644 --- a/server/src/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -1,7 +1,7 @@ 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'; -import { UserTable } from 'src/tables/user.table'; @Table('user_metadata') export class UserMetadataTable implements UserMetadataItem { diff --git a/server/src/tables/user.table.ts b/server/src/schema/tables/user.table.ts similarity index 100% rename from server/src/tables/user.table.ts rename to server/src/schema/tables/user.table.ts diff --git a/server/src/tables/version-history.table.ts b/server/src/schema/tables/version-history.table.ts similarity index 100% rename from server/src/tables/version-history.table.ts rename to server/src/schema/tables/version-history.table.ts diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index efdff0e480..6739678561 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -46,7 +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/tables/user.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; diff --git a/server/src/tables/index.ts b/server/src/tables/index.ts deleted file mode 100644 index 8b92b55187..0000000000 --- a/server/src/tables/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ActivityTable } from 'src/tables/activity.table'; -import { AlbumAssetTable } from 'src/tables/album-asset.table'; -import { AlbumUserTable } from 'src/tables/album-user.table'; -import { AlbumTable } from 'src/tables/album.table'; -import { APIKeyTable } from 'src/tables/api-key.table'; -import { AssetAuditTable } from 'src/tables/asset-audit.table'; -import { AssetFaceTable } from 'src/tables/asset-face.table'; -import { AssetJobStatusTable } from 'src/tables/asset-job-status.table'; -import { AssetTable } from 'src/tables/asset.table'; -import { AuditTable } from 'src/tables/audit.table'; -import { ExifTable } from 'src/tables/exif.table'; -import { FaceSearchTable } from 'src/tables/face-search.table'; -import { GeodataPlacesTable } from 'src/tables/geodata-places.table'; -import { LibraryTable } from 'src/tables/library.table'; -import { MemoryTable } from 'src/tables/memory.table'; -import { MemoryAssetTable } from 'src/tables/memory_asset.table'; -import { MoveTable } from 'src/tables/move.table'; -import { NaturalEarthCountriesTable, NaturalEarthCountriesTempTable } from 'src/tables/natural-earth-countries.table'; -import { PartnerAuditTable } from 'src/tables/partner-audit.table'; -import { PartnerTable } from 'src/tables/partner.table'; -import { PersonTable } from 'src/tables/person.table'; -import { SessionTable } from 'src/tables/session.table'; -import { SharedLinkAssetTable } from 'src/tables/shared-link-asset.table'; -import { SharedLinkTable } from 'src/tables/shared-link.table'; -import { SmartSearchTable } from 'src/tables/smart-search.table'; -import { StackTable } from 'src/tables/stack.table'; -import { SessionSyncCheckpointTable } from 'src/tables/sync-checkpoint.table'; -import { SystemMetadataTable } from 'src/tables/system-metadata.table'; -import { TagAssetTable } from 'src/tables/tag-asset.table'; -import { UserAuditTable } from 'src/tables/user-audit.table'; -import { UserMetadataTable } from 'src/tables/user-metadata.table'; -import { UserTable } from 'src/tables/user.table'; -import { VersionHistoryTable } from 'src/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/test/factory.ts b/server/test/factory.ts index 0becc705bc..ce10095d58 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -35,7 +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/tables/user.table'; +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'; 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; From f4dbfd856e0642424a1a83f9aa08dc4d39ef2435 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 31 Mar 2025 11:47:08 +0200 Subject: [PATCH 12/56] chore(web): update translations (#17115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translation: Immich/immich Co-authored-by: Abhijeet Viswam Co-authored-by: Bezruchenko Simon Co-authored-by: C D Co-authored-by: Henrik Sommerfeld Co-authored-by: Karsten Dambekalns Co-authored-by: Miro Rýzek Co-authored-by: Mohd Nader Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Nergis Co-authored-by: Utkarsh Prajapati Co-authored-by: Yamagishi Kazutoshi Co-authored-by: grgergo --- i18n/ar.json | 4 + i18n/hi.json | 4 + i18n/hu.json | 2 +- i18n/ja.json | 145 +++++++- i18n/ko.json | 73 +++- i18n/lv.json | 8 +- i18n/sk.json | 2 + i18n/sv.json | 2 +- i18n/te.json | 949 ++++++++++++++++++++++++++++++++++++++++++++++++++- i18n/uk.json | 28 +- 10 files changed, 1173 insertions(+), 44 deletions(-) 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/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/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/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/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": "Смітник/Видалити ресурс", From 74f7fd4b5357a5b7561fd0595a9f46622865e6ff Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:48:41 +0200 Subject: [PATCH 13/56] chore: add language requests from weblate (#17236) --- i18n/eu.json | 1 + i18n/gl.json | 1 + i18n/ka.json | 1 + i18n/kk.json | 1 + i18n/kn.json | 1 + i18n/pa.json | 1 + i18n/sq.json | 1 + web/src/lib/constants.ts | 7 +++++++ 8 files changed, 14 insertions(+) create mode 100644 i18n/eu.json create mode 100644 i18n/gl.json create mode 100644 i18n/ka.json create mode 100644 i18n/kk.json create mode 100644 i18n/kn.json create mode 100644 i18n/pa.json create mode 100644 i18n/sq.json 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/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/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/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/web/src/lib/constants.ts b/web/src/lib/constants.ts index 84173fe944..2c21d04865 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -283,10 +283,12 @@ export const langs = [ { name: 'Greek', code: 'el', loader: () => import('$i18n/el.json') }, { name: 'Spanish', code: 'es', loader: () => import('$i18n/es.json') }, { name: 'Estonian', code: 'et', loader: () => import('$i18n/et.json') }, + { name: 'Basque', code: 'eu', loader: () => import('$i18n/eu.json') }, { name: 'Persian', code: 'fa', loader: () => import('$i18n/fa.json') }, { name: 'Finnish', code: 'fi', loader: () => import('$i18n/fi.json') }, { name: 'Filipino', code: 'fil', loader: () => import('$i18n/fil.json') }, { name: 'French', code: 'fr', loader: () => import('$i18n/fr.json') }, + { name: 'Galician', code: 'gl', loader: () => import('$i18n/gl.json') }, { name: 'Hebrew', code: 'he', loader: () => import('$i18n/he.json') }, { name: 'Hindi', code: 'hi', loader: () => import('$i18n/hi.json') }, { name: 'Croatian', code: 'hr', loader: () => import('$i18n/hr.json') }, @@ -295,7 +297,10 @@ export const langs = [ { name: 'Indonesian', code: 'id', loader: () => import('$i18n/id.json') }, { name: 'Italian', code: 'it', loader: () => import('$i18n/it.json') }, { name: 'Japanese', code: 'ja', loader: () => import('$i18n/ja.json') }, + { name: 'Georgian', code: 'ka', loader: () => import('$i18n/ka.json') }, + { name: 'Kazakh', code: 'kk', loader: () => import('$i18n/kk.json') }, { name: 'Kurdish (Northern)', code: 'kmr', loader: () => import('$i18n/kmr.json') }, + { name: 'Kannada', code: 'kn', loader: () => import('$i18n/kn.json') }, { name: 'Korean', code: 'ko', loader: () => import('$i18n/ko.json') }, { name: 'Luxembourgish', code: 'lb', loader: () => import('$i18n/lb.json') }, { name: 'Lithuanian', code: 'lt', loader: () => import('$i18n/lt.json') }, @@ -308,6 +313,7 @@ export const langs = [ { name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$i18n/nb_NO.json') }, { name: 'Dutch', code: 'nl', loader: () => import('$i18n/nl.json') }, { name: 'Norwegian Nynorsk', code: 'nn', loader: () => import('$i18n/nn.json') }, + { name: 'Punjabi', code: 'pa', loader: () => import('$i18n/pa.json') }, { name: 'Polish', code: 'pl', loader: () => import('$i18n/pl.json') }, { name: 'Portuguese', code: 'pt', loader: () => import('$i18n/pt.json') }, { name: 'Portuguese (Brazil) ', code: 'pt-BR', weblateCode: 'pt_BR', loader: () => import('$i18n/pt_BR.json') }, @@ -315,6 +321,7 @@ export const langs = [ { name: 'Russian', code: 'ru', loader: () => import('$i18n/ru.json') }, { name: 'Slovak', code: 'sk', loader: () => import('$i18n/sk.json') }, { name: 'Slovenian', code: 'sl', loader: () => import('$i18n/sl.json') }, + { name: 'Albanian', code: 'sq', loader: () => import('$i18n/sq.json') }, { name: 'Serbian (Cyrillic)', code: 'sr-Cyrl', From e4f83680d9ada606d2b100c88adcb83720f4c8e8 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 31 Mar 2025 13:08:41 +0200 Subject: [PATCH 14/56] feat: use my.immich.app for externalDomain fallback (#17209) * feat: use my.immich.app for externalDomain fallback This is probably more useful than localhost. * chore: remove port param * fix: update expected value in tests * fix: update expected value in e2e --- e2e/src/api/specs/shared-link.e2e-spec.ts | 2 +- .../src/services/notification.service.spec.ts | 4 ++-- server/src/services/notification.service.ts | 19 +++++++------------ .../src/services/shared-link.service.spec.ts | 4 ++-- server/src/services/shared-link.service.ts | 3 +-- server/src/utils/misc.ts | 3 +-- 6 files changed, 14 insertions(+), 21 deletions(-) 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(` { 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/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 From 238c151ac381b7911ccae864322b36a6a4dc0cea Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:18:25 +0200 Subject: [PATCH 15/56] chore: finish migrating eslint config files; bump unicorn (#17200) --- cli/eslint.config.mjs | 30 +- cli/package-lock.json | 792 +++++++----------- cli/package.json | 5 +- e2e/eslint.config.mjs | 30 +- e2e/package-lock.json | 783 +++++++---------- e2e/package.json | 5 +- e2e/src/cli/specs/upload.e2e-spec.ts | 10 +- e2e/src/utils.ts | 1 + server/eslint.config.mjs | 30 +- server/package-lock.json | 448 +++++----- server/package.json | 5 +- server/src/bin/sync-sql.ts | 2 +- .../machine-learning.repository.ts | 4 +- .../notification.repository.spec.ts | 1 + server/src/repositories/person.repository.ts | 4 +- .../repositories/storage.repository.spec.ts | 1 + server/src/repositories/user.repository.ts | 1 - server/src/services/api.service.ts | 4 +- server/src/services/backup.service.ts | 2 +- server/src/utils/access.ts | 4 - server/test/factory.ts | 1 + .../medium/specs/metadata.service.spec.ts | 1 + server/test/utils.ts | 5 + 23 files changed, 883 insertions(+), 1286 deletions(-) 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..f4387c5c04 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -28,8 +28,6 @@ "@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", "@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", @@ -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" } @@ -1525,20 +1374,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "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.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1558,16 +1408,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { @@ -1583,14 +1433,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1601,14 +1451,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1625,9 +1475,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { @@ -1639,14 +1489,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1666,16 +1516,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1690,13 +1540,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1974,9 +1824,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 +1857,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 +1907,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 +1986,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 +1996,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -2218,13 +2070,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 +2139,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 +2151,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", @@ -2484,28 +2327,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 +2357,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": { @@ -2793,6 +2636,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 +2701,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 +2767,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 +2821,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 +2974,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 +2990,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 +3009,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 +3044,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 +3146,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 +3226,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 +3287,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 +3306,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 +3341,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 +3512,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 +3573,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 +3678,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 +3744,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 +3754,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", @@ -4103,15 +3843,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,18 +3882,6 @@ "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", @@ -4288,6 +4020,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 +4047,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz", + "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.28.0", + "@typescript-eslint/parser": "8.28.0", + "@typescript-eslint/utils": "8.28.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 +4077,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 +4112,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -4354,6 +4135,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" diff --git a/cli/package.json b/cli/package.json index 63c55ed790..8f0b0a337c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -22,8 +22,6 @@ "@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", "@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/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index fd1e8a0af6..f73e31e48e 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/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', @@ -62,4 +52,4 @@ export default [ 'object-shorthand': ['error', 'always'], }, }, -]; +]); diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6302062ef5..b09d01e75e 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -20,13 +20,11 @@ "@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", @@ -39,6 +37,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" } @@ -68,8 +67,6 @@ "@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", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -77,12 +74,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", @@ -129,89 +127,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", @@ -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", @@ -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" } @@ -1754,7 +1596,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", @@ -1906,17 +1749,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "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.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1936,16 +1779,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { @@ -1961,14 +1804,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1979,14 +1822,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2003,9 +1846,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { @@ -2017,14 +1860,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2070,16 +1913,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2094,13 +1937,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2465,9 +2308,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 +2341,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 +2453,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 +2541,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 +2551,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -2843,13 +2688,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 +2875,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 +2919,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", @@ -3286,28 +3122,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 +3152,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": { @@ -3634,6 +3470,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 +3861,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 +4034,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 +4075,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 +4227,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 +4265,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 +4399,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", @@ -4733,6 +4584,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 +4737,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 +4955,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 +4974,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 +5027,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 +5458,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 +5520,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": { @@ -5938,9 +5693,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 +5835,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 +5845,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", @@ -6202,15 +5961,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,18 +6035,6 @@ "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", @@ -6497,6 +6248,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 +6288,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz", + "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.28.0", + "@typescript-eslint/parser": "8.28.0", + "@typescript-eslint/utils": "8.28.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 +6318,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 +6341,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 +6362,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6605,6 +6405,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" diff --git a/e2e/package.json b/e2e/package.json index 25db3f2902..0ab32ca445 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -30,13 +30,11 @@ "@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/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index 301c6d3bf0..c901a299ab 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -22,7 +22,7 @@ const tests: Test[] = [ }, { test: 'should support paths with an asterisk', - paths: [`/photos\*/image1.jpg`], + paths: [`/photos*/image1.jpg`], files: { '/photos*/image1.jpg': true, '/photos*/image2.jpg': false, @@ -40,7 +40,7 @@ const tests: Test[] = [ }, { test: 'should support paths with a single quote', - paths: [`/photos\'/image1.jpg`], + paths: [`/photos'/image1.jpg`], files: { "/photos'/image1.jpg": true, "/photos'/image2.jpg": false, @@ -49,7 +49,7 @@ const tests: Test[] = [ }, { test: 'should support paths with a double quote', - paths: [`/photos\"/image1.jpg`], + paths: [`/photos"/image1.jpg`], files: { '/photos"/image1.jpg': true, '/photos"/image2.jpg': false, @@ -67,7 +67,7 @@ const tests: Test[] = [ }, { test: 'should support paths with an opening brace', - paths: [`/photos\{/image1.jpg`], + paths: [`/photos{/image1.jpg`], files: { '/photos{/image1.jpg': true, '/photos{/image2.jpg': false, @@ -76,7 +76,7 @@ const tests: Test[] = [ }, { test: 'should support paths with a closing brace', - paths: [`/photos\}/image1.jpg`], + paths: [`/photos}/image1.jpg`], files: { '/photos}/image1.jpg': true, '/photos}/image2.jpg': false, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 81539a8e00..08b29a4a11 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -537,6 +537,7 @@ export const utils = { }, waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => { + // 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/server/eslint.config.mjs b/server/eslint.config.mjs index b1e7d409b1..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', @@ -87,4 +77,4 @@ export default [ ], }, }, -]; +]); diff --git a/server/package-lock.json b/server/package-lock.json index 8e06a8bf66..bd27793d44 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -97,13 +97,11 @@ "@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", @@ -117,6 +115,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", @@ -6069,17 +6068,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "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.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6099,16 +6098,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { @@ -6124,14 +6123,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6142,14 +6141,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -6166,9 +6165,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { @@ -6180,14 +6179,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6233,16 +6232,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "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.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6257,13 +6256,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.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -7417,13 +7416,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" @@ -9048,28 +9047,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" @@ -9078,7 +9077,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": { @@ -9618,6 +9617,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", @@ -10371,9 +10383,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" }, @@ -10541,13 +10566,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": { @@ -10691,16 +10732,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" @@ -12217,26 +12258,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": { @@ -12478,16 +12511,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", @@ -13563,114 +13586,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": { @@ -13817,25 +13812,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": { @@ -15039,16 +15038,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": { @@ -16275,6 +16277,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz", + "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.28.0", + "@typescript-eslint/parser": "8.28.0", + "@typescript-eslint/utils": "8.28.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", @@ -16381,6 +16406,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", diff --git a/server/package.json b/server/package.json index 2c5959b7af..5125b5ae55 100644 --- a/server/package.json +++ b/server/package.json @@ -123,13 +123,11 @@ "@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", @@ -143,6 +141,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/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/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 5e916c71f3..95aa4cff1e 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -91,7 +91,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; } 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 01b45bd94b..751f97fdeb 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -84,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 }] }) @@ -269,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/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/user.repository.ts b/server/src/repositories/user.repository.ts index c619063d04..c254085fd2 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -236,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/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/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/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/test/factory.ts b/server/test/factory.ts index ce10095d58..028b530255 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -216,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/medium/specs/metadata.service.spec.ts b/server/test/medium/specs/metadata.service.spec.ts index 28f2c9f64f..5613a05fd0 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 }), ); 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), From 09f4476f97f1c45c3befe0edf33173721b77182b Mon Sep 17 00:00:00 2001 From: PathToLife <12622625+PathToLife@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:28:41 +1300 Subject: [PATCH 16/56] feat: improve performance for GET /api/album & /api/album/:id (#17124) * fix(server) optimize number of sql calls for GET /api/albums remove unnecessary join for getMetadataForIds remove separate call to getLastUpdatedAssetForAlbumId * fix(server) remove unnecessary getLastUpdatedAssetForAlbumId call for GET /api/album/:id also remove getLastUpdatedAssetForAlbumId query as it is no longer referenced * fix(server): correct lastModifiedAssetTimestamp return type + formatting and typing * chore(server): address type issue with tests found via npm:check tests & lint still pass before this commit. --- server/src/queries/album.repository.sql | 20 ++++++---- server/src/repositories/album.repository.ts | 28 ++++++++------ server/src/repositories/asset.repository.ts | 11 ------ server/src/services/album.service.spec.ts | 37 +++++++++++++++++-- server/src/services/album.service.ts | 25 +++++-------- .../repositories/asset.repository.mock.ts | 1 - 6 files changed, 71 insertions(+), 51 deletions(-) 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/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..9fd1ce6b84 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -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') 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/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 531f8d56f1..1114a70f9f 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 Date: Mon, 31 Mar 2025 06:30:56 -0500 Subject: [PATCH 17/56] fix: MAX_PARAMETERS_EXCEEDED error during person cleanup job (#17222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add batch size in sql delete,fix person cleanup error: ERROR [Microservices:{}] Unable to run job handler (backgroundTask/person-cleanup): Error: MAX_PARAMETERS_EXCEEDED: Max number of parameters (65534) exceeded * add chunked decorator to delete * chore: prettier formatting fixes --------- Co-authored-by: hwang3419 <“hwang.iit@gmail.com”> Co-authored-by: Zack Pollard --- server/src/services/person.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index c6c3ce4e4f..65a4508a3b 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 { @@ -241,6 +241,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); From d613f15606f1333aa674119ffd8129d8d24388cb Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Mon, 31 Mar 2025 13:19:57 +0100 Subject: [PATCH 18/56] test: fix flaky user handle delete check medium test (#17253) we can't run specifically the handleUserDeleteCheck tests concurrently due to one of the tests modifying the config in the shared database if run concurrently you can get race conditions where the other tests pick up the change, even with resetting the config in the beforeEach therefore the test that checks a delete actually happens, fails there are many ways to solve this, disabling concurrency for the suite, forcing sequential tests for just handleUserDeleteCheck, increasing the delete test deletedAt to more than the custom duration tests deleteDelay I applied all three of these. You could also force all the user tests to run in their own databases, but that feels overkill --- server/test/medium/specs/user.service.spec.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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([]); }); }); }); From b25914c2a50487ba4d774c655a57ba7ba8e5939a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:15:52 -0700 Subject: [PATCH 19/56] chore: use writable derived in more places (#17248) chore(web): use writable derived in more places --- .../lib/components/album-page/album-title.svelte | 6 +----- web/src/lib/components/elements/date-input.svelte | 5 +---- .../shared-components/autogrow-textarea.svelte | 5 +---- .../components/shared-components/combobox.svelte | 6 +----- .../components/shared-components/star-rating.svelte | 6 +----- .../components/shared-components/tree/tree.svelte | 13 +++++-------- web/src/routes/auth/register/+page.svelte | 10 ++++------ 7 files changed, 14 insertions(+), 37 deletions(-) diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 74786c1ea4..0c712e426c 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -13,11 +13,7 @@ let { id, albumName = $bindable(), isOwned, onUpdate }: Props = $props(); - let newAlbumName = $state(albumName); - - $effect(() => { - newAlbumName = albumName; - }); + let newAlbumName = $derived(albumName); const handleUpdateName = async () => { if (newAlbumName === albumName) { diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/components/elements/date-input.svelte index 687e9442e7..30f404079c 100644 --- a/web/src/lib/components/elements/date-input.svelte +++ b/web/src/lib/components/elements/date-input.svelte @@ -16,10 +16,7 @@ // Updating `value` directly causes the date input to reset itself or // interfere with user changes. - let updatedValue = $state(); - $effect(() => { - updatedValue = value; - }); + let updatedValue = $derived(value); null, placeholder = '' }: Props = $props(); - let newContent = $state(content); - $effect(() => { - newContent = content; - }); + let newContent = $derived(content); const updateContent = () => { if (content === newContent) { diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index c2284ebb0c..3b70b0e859 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -71,7 +71,7 @@ * Keeps track of whether the combobox is actively being used. */ let isActive = $state(false); - let searchQuery = $state(selectedOption?.label || ''); + let searchQuery = $derived(selectedOption?.label || ''); let selectedIndex: number | undefined = $state(); let optionRefs: HTMLElement[] = $state([]); let input = $state(); @@ -228,10 +228,6 @@ const getInputPosition = () => input?.getBoundingClientRect(); - $effect(() => { - searchQuery = selectedOption ? selectedOption.label : ''; - }); - let filteredOptions = $derived.by(() => { const _options = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())); diff --git a/web/src/lib/components/shared-components/star-rating.svelte b/web/src/lib/components/shared-components/star-rating.svelte index bf9e5fbcaf..8dfd3af1f3 100644 --- a/web/src/lib/components/shared-components/star-rating.svelte +++ b/web/src/lib/components/shared-components/star-rating.svelte @@ -14,15 +14,11 @@ let { count = 5, rating, readOnly = false, onRating }: Props = $props(); - let ratingSelection = $state(rating); + let ratingSelection = $derived(rating); let hoverRating = $state(0); let focusRating = $state(0); let timeoutId: ReturnType | undefined; - $effect(() => { - ratingSelection = rating; - }); - const starIcon = 'M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z'; const id = generateId(); diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index ccc4181abe..33f9d14a13 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -16,14 +16,11 @@ let { tree, parent, value, active = '', icons, getLink, getColor }: Props = $props(); - let path = $derived(normalizeTreePath(`${parent}/${value}`)); - let isActive = $derived(active === path || active.startsWith(`${path}/`)); - let isOpen = $state(false); - $effect(() => { - isOpen = isActive; - }); - let isTarget = $derived(active === path); - let color = $derived(getColor(path)); + const path = $derived(normalizeTreePath(`${parent}/${value}`)); + const isActive = $derived(active === path || active.startsWith(`${path}/`)); + const isTarget = $derived(active === path); + const color = $derived(getColor(path)); + let isOpen = $derived(isActive); const onclick = (event: MouseEvent) => { event.preventDefault(); diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index f3bc494d95..4ec1b9718e 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -13,8 +13,10 @@ let password = $state(''); let confirmPassword = $state(''); let name = $state(''); - let errorMessage = $state(''); - let valid = $derived(password === confirmPassword && confirmPassword.length > 0); + let errorMessage = $derived( + password === confirmPassword || confirmPassword.length === 0 ? '' : $t('password_does_not_match'), + ); + const valid = $derived(password === confirmPassword && confirmPassword.length > 0); interface Props { data: PageData; @@ -22,10 +24,6 @@ let { data }: Props = $props(); - $effect(() => { - errorMessage = password === confirmPassword || confirmPassword.length === 0 ? '' : $t('password_does_not_match'); - }); - const onSubmit = async (event: Event) => { event.preventDefault(); From b8b2898c8713398b3d6f1e913b2ca44d8029e6fa Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 31 Mar 2025 09:16:04 -0500 Subject: [PATCH 20/56] fix(server): double extension when filename has uppercase extension (#17226) * fix(server): double extension when filename has uppercase extension * Proper tests --- .../services/storage-template.service.spec.ts | 98 +++++++++++++++++++ .../src/services/storage-template.service.ts | 4 +- 2 files changed, 101 insertions(+), 1 deletion(-) 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 }); From faabda4446322ffd5d8f325a02acf2a63185842c Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Mon, 31 Mar 2025 16:16:30 +0200 Subject: [PATCH 21/56] fix(server): multiple exclusion patterns (#17221) --- e2e/src/api/specs/library.e2e-spec.ts | 79 ++++++++++++++++++++- server/src/repositories/asset.repository.ts | 2 +- 2 files changed, 78 insertions(+), 3 deletions(-) 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/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 9fd1ce6b84..35a5ab21a9 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1053,7 +1053,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(); From efcb1129ceb14dcbf3218b7325fce67820579d19 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Mon, 31 Mar 2025 16:16:53 +0200 Subject: [PATCH 22/56] fix(server): don't sync null date assets (#17247) --- server/src/queries/asset.repository.sql | 6 ++++++ server/src/repositories/asset.repository.ts | 6 ++++++ 2 files changed, 12 insertions(+) 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/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 35a5ab21a9..e2e1bd9a6f 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -967,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') @@ -995,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; From 8c50e3e80e97d0855792000722bd7f97093760ae Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:17:57 -0400 Subject: [PATCH 23/56] feat(server): consider `JpgFromRaw2` tag for embedded previews (#17123) * add jpgfromraw2 * unused catch --- server/src/repositories/media.repository.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 483bd3fd90..eb43b4335b 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -43,14 +43,18 @@ 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); + await exiftool.extractBinaryTag('JpgFromRaw2', input, output); + } catch { try { - await exiftool.extractPreview(input, output); + 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; + } } } From d71c5602c39854a96786b8c2a42fd16e2cac7bdb Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Mon, 31 Mar 2025 09:34:43 -0500 Subject: [PATCH 24/56] fix(server): Postgres error pretty printing (#17204) * add patch-package to dev dependencies this allows us to patch upstream packages without waiting for PRs to be merged (or not!). Patch-package does a pretty good job of notifying if upstream does a change to invalidate the patch (its a git patch under the hood). * Patch implementation of https://github.com/porsager/postgres/pull/944 This PR has not been merged by upstream and helps produce verbose error messages when postgres fails to connect (usually incorrect credentials). This is in contrast to error messages such as `TypeError: Cannot read properties of undefined (reading 'replace'), stack: TypeError: Cannot read properties of undefined (reading 'replace')` * have postinstall only run when not installing a global package (such as immich-cli in the Docker build) --- server/package-lock.json | 309 ++++++++++++++++++++++++++++ server/package.json | 4 +- server/patches/postgres+3.4.5.patch | 39 ++++ 3 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 server/patches/postgres+3.4.5.patch diff --git a/server/package-lock.json b/server/package-lock.json index bd27793d44..59074a2c6e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "immich", "version": "1.130.3", + "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -106,6 +107,7 @@ "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", @@ -6617,6 +6619,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", @@ -7083,6 +7092,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", @@ -7526,6 +7545,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", @@ -8320,6 +8358,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", @@ -9617,6 +9673,16 @@ "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/find-up-simple": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", @@ -10328,6 +10394,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", @@ -10762,6 +10841,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", @@ -10869,6 +10964,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", @@ -11098,6 +11206,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", @@ -11105,6 +11233,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", @@ -11137,6 +11272,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", @@ -11147,6 +11292,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", @@ -12330,6 +12485,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", @@ -12384,6 +12549,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", @@ -12575,6 +12757,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", @@ -14283,6 +14564,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", @@ -14562,6 +14861,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", diff --git a/server/package.json b/server/package.json index 5125b5ae55..cc41a53db2 100644 --- a/server/package.json +++ b/server/package.json @@ -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", @@ -132,6 +133,7 @@ "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", 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 }, From 838a8dd9a6ec14506186347c04317f7360050e21 Mon Sep 17 00:00:00 2001 From: PathToLife <12622625+PathToLife@users.noreply.github.com> Date: Tue, 1 Apr 2025 03:45:30 +1300 Subject: [PATCH 25/56] feat(web): increase album collapse click area (#17213) --- web/src/lib/components/album-page/album-card-group.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ {#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/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index b80e7d1a44..a1a24634c4 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -7,10 +7,17 @@ * Target for the skip link to move focus to. */ target?: string; + /** + * Text for the skip link button. + */ text?: string; + /** + * Breakpoint at which the skip link is visible. Defaults to always being visible. + */ + breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; } - let { target = 'main', text = $t('skip_to_content') }: Props = $props(); + let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props(); let isFocused = $state(false); @@ -18,6 +25,29 @@ const targetEl = document.querySelector(target); targetEl?.focus(); }; + + const getBreakpoint = () => { + if (!breakpoint) { + return ''; + } + switch (breakpoint) { + case 'sm': { + return 'hidden sm:block'; + } + case 'md': { + return 'hidden md:block'; + } + case 'lg': { + return 'hidden lg:block'; + } + case 'xl': { + return 'hidden xl:block'; + } + case '2xl': { + return 'hidden 2xl:block'; + } + } + };
@@ -25,6 +55,7 @@ size="sm" rounded="none" onclick={moveFocus} + class={getBreakpoint()} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} > diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 27873f39a5..4944982b60 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -51,7 +51,7 @@
{#if sidebar}{@render sidebar()}{:else if admin} @@ -66,7 +66,7 @@ >
{#if title} -
{title}
+
{title}
{/if} {#if description}

{description}

diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index d3afdc6072..7f716e70ef 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -726,7 +726,7 @@ class={[ 'scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, - { 'ml-4 tall:ml-0': !isEmpty }, + { 'ml-0': !isEmpty }, { 'mr-[60px]': !isEmpty && !usingMobileDevice }, ]} tabindex="-1" diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 9536aaf746..ff9264b961 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -38,7 +38,7 @@
(offsetWidth = width)} onscroll={onScroll} diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 02b55a1d07..bd4bffd2f6 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -1,3 +1,7 @@ + + -
+

{$t('purchase_individual_title')}

diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index 6a4e7f1a4b..567fce9281 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -57,7 +57,7 @@
{/if} -
+
diff --git a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index ffc015233c..19db461229 100644 --- a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -8,7 +8,9 @@ -
+

{$t('purchase_server_title')}

diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index a42e340eae..47e46c59b5 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -78,7 +78,7 @@ (isOpen = false)} /> {/if} -