diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 24e0859d5..3da7cbe10 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -45,7 +45,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.16.9" + flutter-version: "3.19.3" cache: true - name: Create the Keystore diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 000dbe2dd..9d50f6f8f 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -4,16 +4,16 @@ on: workflow_dispatch: inputs: serverBump: - description: "Bump server version" + description: 'Bump server version' required: true - default: "false" + default: 'false' type: choice options: - - "false" + - 'false' - minor - patch mobileBump: - description: "Bump mobile build number" + description: 'Bump mobile build number' required: false type: boolean @@ -46,8 +46,8 @@ jobs: with: author_name: Alex The Bot author_email: alex.tran1502@gmail.com - default_author: user_info - message: "Version ${{ env.IMMICH_VERSION }}" + default_author: user_info + message: 'Version ${{ env.IMMICH_VERSION }}' tag: ${{ env.IMMICH_VERSION }} push: true @@ -85,4 +85,5 @@ jobs: docker/example.env docker/hwaccel.ml.yml docker/hwaccel.transcoding.yml + docker/prometheus.yml *.apk diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index c2b633c4b..1fb9cb55f 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -23,7 +23,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.16.9" + flutter-version: "3.19.3" - name: Install dependencies run: dart pub get diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d57b3a84..6a5df111d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -329,14 +329,14 @@ jobs: - name: Generate new migrations continue-on-error: true - run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration + run: npm run typeorm:migrations:generate ./src/migrations/TestMigration - name: Find file changes uses: tj-actions/verify-changed-files@v19 id: verify-changed-files with: files: | - server/src/infra/migrations/ + server/src/migrations/ - name: Verify migration files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' run: | @@ -354,7 +354,7 @@ jobs: id: verify-changed-sql-files with: files: | - server/src/infra/sql + server/src/queries - name: Verify SQL files have not changed if: steps.verify-changed-sql-files.outputs.files_changed == 'true' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..3267c6713 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + "editor.formatOnSave": true, + "[javascript][typescript][css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.formatOnSave": true + }, + "[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.tabSize": 2 + }, + "svelte.enable-ts-plugin": true, + "eslint.validate": [ + "javascript", + "svelte" + ], + "typescript.preferences.importModuleSpecifier": "non-relative", + "[dart]": { + "editor.formatOnSave": true, + "editor.selectionHighlight": false, + "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.suggestSelection": "first", + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": "off", + "editor.defaultFormatter": "Dart-Code.dart-code" + }, + "cSpell.words": [ + "immich" + ], + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.ts": "${capture}.spec.ts,${capture}.mock.ts" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 255013501..cc84d81c5 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@

- Català - Español - Français - Italiano - 日本語 - 한국어 - Deutsch - Nederlands - Türkçe - 中文 - Русский + Català + Español + Français + Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文 + Русский

## Disclaimer @@ -131,6 +131,10 @@ If you feel like this is the right cause and the app is something you are seeing ## Star History - - Star History Chart + + + + + Star History Chart + diff --git a/cli/Dockerfile b/cli/Dockerfile index cb0383a00..6195d8d82 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c as core +FROM node:20-alpine3.19@sha256:ef3f47741e161900ddd07addcaca7e76534a9205e4cd73b2ed091ba339004a75 as core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index 69be80132..6edc44424 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.1.0", + "version": "2.2.0", "license": "GNU Affero General Public License version 3", "dependencies": { "lodash-es": "^4.17.21" @@ -47,7 +47,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -300,9 +300,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -316,9 +316,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -332,9 +332,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -348,9 +348,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -380,9 +380,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -412,9 +412,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -428,9 +428,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -460,9 +460,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -476,9 +476,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -492,9 +492,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -508,9 +508,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -524,9 +524,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -540,9 +540,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -556,9 +556,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -572,9 +572,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -588,9 +588,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -604,9 +604,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -620,9 +620,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -636,9 +636,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -652,9 +652,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -1230,9 +1230,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", - "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1251,16 +1251,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", - "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/type-utils": "7.2.0", - "@typescript-eslint/utils": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1269,7 +1269,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1286,19 +1286,19 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", - "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1314,16 +1314,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", - "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1331,18 +1331,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", - "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1358,12 +1358,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", - "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1371,13 +1371,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", - "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1386,7 +1386,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1399,21 +1399,21 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1424,16 +1424,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1447,9 +1447,9 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", - "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz", + "integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -1457,12 +1457,13 @@ "debug": "^4.3.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^4.0.1", + "istanbul-lib-source-maps": "^5.0.4", "istanbul-reports": "^3.1.6", "magic-string": "^0.30.5", "magicast": "^0.3.3", "picocolors": "^1.0.0", "std-env": "^3.5.0", + "strip-literal": "^2.0.0", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.2.0" }, @@ -1470,17 +1471,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.3.1" + "vitest": "1.4.0" } }, "node_modules/@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", + "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", "dev": true, "dependencies": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "chai": "^4.3.10" }, "funding": { @@ -1488,12 +1489,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", + "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", "dev": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.4.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -1529,9 +1530,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", + "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -1543,9 +1544,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", + "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -1555,9 +1556,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2037,9 +2038,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -2049,29 +2050,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { @@ -2858,14 +2859,14 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", "dev": true, "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" @@ -3488,9 +3489,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -3509,7 +3510,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3987,19 +3988,10 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4376,9 +4368,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -4464,14 +4456,14 @@ } }, "node_modules/vite": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", - "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", + "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.36", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -4519,9 +4511,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", + "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -4560,16 +4552,16 @@ } }, "node_modules/vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", + "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", "dev": true, "dependencies": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/expect": "1.4.0", + "@vitest/runner": "1.4.0", + "@vitest/snapshot": "1.4.0", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -4583,7 +4575,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.3.1", + "vite-node": "1.4.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -4598,8 +4590,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.1", - "@vitest/ui": "1.3.1", + "@vitest/browser": "1.4.0", + "@vitest/ui": "1.4.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/cli/package.json b/cli/package.json index 45c60569e..d33de8918 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.1.0", + "version": "2.2.0", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index b6c159c9b..aa45ce547 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,5 +1,7 @@ import { + Action, AssetBulkUploadCheckResult, + AssetFileUploadResponseDto, addAssetsToAlbum, checkBulkUpload, createAlbum, @@ -8,445 +10,320 @@ import { getSupportedMediaTypes, } from '@immich/sdk'; import byteSize from 'byte-size'; -import cliProgress from 'cli-progress'; -import { chunk, zip } from 'lodash-es'; -import { createHash } from 'node:crypto'; -import fs, { createReadStream } from 'node:fs'; -import { access, constants, stat, unlink } from 'node:fs/promises'; +import { Presets, SingleBar } from 'cli-progress'; +import { chunk } from 'lodash-es'; +import { Stats, createReadStream } from 'node:fs'; +import { stat, unlink } from 'node:fs/promises'; import os from 'node:os'; -import { basename } from 'node:path'; -import { CrawlService } from 'src/services/crawl.service'; -import { BaseOptions, authenticate } from 'src/utils'; +import path, { basename } from 'node:path'; +import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils'; -const zipDefined = zip as (a: T[], b: U[]) => [T, U][]; +const s = (count: number) => (count === 1 ? '' : 's'); -enum CheckResponseStatus { - ACCEPT = 'accept', - REJECT = 'reject', - DUPLICATE = 'duplicate', -} +// TODO figure out why `id` is missing +type AssetBulkUploadCheckResults = Array; +type Asset = { id: string; filepath: string }; -class Asset { - readonly path: string; - - id?: string; - deviceAssetId?: string; - fileCreatedAt?: Date; - fileModifiedAt?: Date; - sidecarPath?: string; - fileSize?: number; +interface UploadOptionsDto { + recursive?: boolean; + exclusionPatterns?: string[]; + dryRun?: boolean; + skipHash?: boolean; + delete?: boolean; + album?: boolean; albumName?: string; + includeHidden?: boolean; + concurrency: number; +} - constructor(path: string) { - this.path = path; +class UploadFile extends File { + constructor( + private filepath: string, + private _size: number, + ) { + super([], basename(filepath)); } - async prepare() { - const stats = await stat(this.path); - this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, ''); - this.fileCreatedAt = stats.mtime; - this.fileModifiedAt = stats.mtime; - this.fileSize = stats.size; - this.albumName = this.extractAlbumName(); + get size() { + return this._size; } - async getUploadFormData(): Promise { - if (!this.deviceAssetId) { - throw new Error('Device asset id not set'); - } - if (!this.fileCreatedAt) { - throw new Error('File created at not set'); - } - if (!this.fileModifiedAt) { - throw new Error('File modified at not set'); - } - - // TODO: doesn't xmp replace the file extension? Will need investigation - const sideCarPath = `${this.path}.xmp`; - let sidecarData: Blob | undefined = undefined; - try { - await access(sideCarPath, constants.R_OK); - sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath)); - } catch {} - - const data: any = { - assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)), - deviceAssetId: this.deviceAssetId, - deviceId: 'CLI', - fileCreatedAt: this.fileCreatedAt.toISOString(), - fileModifiedAt: this.fileModifiedAt.toISOString(), - isFavorite: String(false), - }; - const formData = new FormData(); - - for (const property in data) { - formData.append(property, data[property]); - } - - if (sidecarData) { - formData.append('sidecarData', sidecarData); - } - - return formData; - } - - async delete(): Promise { - return unlink(this.path); - } - - public async hash(): Promise { - const sha1 = (filePath: string) => { - const hash = createHash('sha1'); - return new Promise((resolve, reject) => { - const rs = createReadStream(filePath); - rs.on('error', reject); - rs.on('data', (chunk) => hash.update(chunk)); - rs.on('end', () => resolve(hash.digest('hex'))); - }); - }; - - return await sha1(this.path); - } - - private extractAlbumName(): string | undefined { - return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2); + stream() { + return createReadStream(this.filepath) as any; } } -class UploadOptionsDto { - recursive? = false; - exclusionPatterns?: string[] = []; - dryRun? = false; - skipHash? = false; - delete? = false; - album? = false; - albumName? = ''; - includeHidden? = false; - concurrency? = 4; -} +export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { + await authenticate(baseOptions); -export const upload = (paths: string[], baseOptions: BaseOptions, uploadOptions: UploadOptionsDto) => - new UploadCommand().run(paths, baseOptions, uploadOptions); + const files = await scan(paths, options); + if (files.length === 0) { + console.log('No files found, exiting'); + return; + } -// TODO refactor this -class UploadCommand { - public async run(paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto): Promise { - await authenticate(baseOptions); + const { newFiles, duplicates } = await checkForDuplicates(files, options); - console.log('Crawling for assets...'); - const files = await this.getFiles(paths, options); + const newAssets = await uploadFiles(newFiles, options); + await updateAlbums([...newAssets, ...duplicates], options); + await deleteFiles(newFiles, options); +}; - if (files.length === 0) { - console.log('No assets found, exiting'); - return; +const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => { + const { image, video } = await getSupportedMediaTypes(); + + console.log('Crawling for assets...'); + const files = await crawl({ + pathsToCrawl, + recursive: options.recursive, + exclusionPatterns: options.exclusionPatterns, + includeHidden: options.includeHidden, + extensions: [...image, ...video], + }); + + return files; +}; + +const checkForDuplicates = async (files: string[], { concurrency }: UploadOptionsDto) => { + const progressBar = new SingleBar( + { format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + + progressBar.start(files.length, 0); + + const newFiles: string[] = []; + const duplicates: Asset[] = []; + + try { + // TODO refactor into a queue + for (const items of chunk(files, concurrency)) { + const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) }))); + const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } }); + + for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) { + if (action === Action.Accept) { + newFiles.push(filepath); + } else { + // rejects are always duplicates + duplicates.push({ id: assetId as string, filepath }); + } + progressBar.increment(); + } } + } finally { + progressBar.stop(); + } - const assetsToCheck = files.map((path) => new Asset(path)); + console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`); - const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4); + return { newFiles, duplicates }; +}; - const totalSizeUploaded = await this.upload(newAssets, options); - const messageStart = options.dryRun ? 'Would have' : 'Successfully'; - if (newAssets.length === 0) { - console.log('All assets were already uploaded, nothing to do.'); - } else { - console.log( - `${messageStart} uploaded ${newAssets.length} asset${newAssets.length === 1 ? '' : 's'} (${byteSize(totalSizeUploaded)})`, +const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise => { + if (files.length === 0) { + console.log('All assets were already uploaded, nothing to do.'); + return []; + } + + // Compute total size first + let totalSize = 0; + const statsMap = new Map(); + for (const filepath of files) { + const stats = await stat(filepath); + statsMap.set(filepath, stats); + totalSize += stats.size; + } + + if (dryRun) { + console.log(`Would have uploaded ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`); + return []; + } + + const uploadProgress = new SingleBar( + { format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' }, + Presets.shades_classic, + ); + uploadProgress.start(totalSize, 0); + uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); + + let totalSizeUploaded = 0; + const newAssets: Asset[] = []; + try { + for (const items of chunk(files, concurrency)) { + await Promise.all( + items.map(async (filepath) => { + const stats = statsMap.get(filepath) as Stats; + const response = await uploadFile(filepath, stats); + totalSizeUploaded += stats.size ?? 0; + uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) }); + newAssets.push({ id: response.id, filepath }); + return response; + }), ); } + } finally { + uploadProgress.stop(); + } - if (options.album || options.albumName) { - const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums( - [...newAssets, ...duplicateAssets], - options, + console.log(`Successfully uploaded ${newAssets.length} asset${s(newAssets.length)} (${byteSize(totalSizeUploaded)})`); + return newAssets; +}; + +const uploadFile = async (input: string, stats: Stats): Promise => { + const { baseUrl, headers } = defaults; + + const assetPath = path.parse(input); + const noExtension = path.join(assetPath.dir, assetPath.name); + + const sidecarsFiles = await Promise.all( + // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp + [`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => { + try { + const stats = await stat(sidecarPath); + return new UploadFile(sidecarPath, stats.size); + } catch { + return false; + } + }), + ); + + const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false); + + const formData = new FormData(); + formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, '')); + formData.append('deviceId', 'CLI'); + formData.append('fileCreatedAt', stats.mtime.toISOString()); + formData.append('fileModifiedAt', stats.mtime.toISOString()); + formData.append('fileSize', String(stats.size)); + formData.append('isFavorite', 'false'); + formData.append('assetData', new UploadFile(input, stats.size)); + + if (sidecarData) { + formData.append('sidecarData', sidecarData); + } + + const response = await fetch(`${baseUrl}/asset/upload`, { + method: 'post', + redirect: 'error', + headers: headers as Record, + body: formData, + }); + if (response.status !== 200 && response.status !== 201) { + throw new Error(await response.text()); + } + + return response.json(); +}; + +const deleteFiles = async (files: string[], options: UploadOptionsDto): Promise => { + if (!options.delete) { + return; + } + + if (options.dryRun) { + console.log(`Would now have deleted assets, but skipped due to dry run`); + return; + } + + console.log('Deleting assets that have been uploaded...'); + + const deletionProgress = new SingleBar( + { format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + deletionProgress.start(files.length, 0); + + try { + for (const assetBatch of chunk(files, options.concurrency)) { + await Promise.all(assetBatch.map((input: string) => unlink(input))); + deletionProgress.update(assetBatch.length); + } + } finally { + deletionProgress.stop(); + } +}; + +const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => { + if (!options.album && !options.albumName) { + return; + } + const { dryRun, concurrency } = options; + + const albums = await getAllAlbums({}); + const existingAlbums = new Map(albums.map((album) => [album.albumName, album.id])); + const newAlbums: Set = new Set(); + for (const { filepath } of assets) { + const albumName = getAlbumName(filepath, options); + if (albumName && !existingAlbums.has(albumName)) { + newAlbums.add(albumName); + } + } + + if (dryRun) { + // TODO print asset counts for new albums + console.log(`Would have created ${newAlbums.size} new album${s(newAlbums.size)}`); + console.log(`Would have updated ${assets.length} asset${s(assets.length)}`); + return; + } + + const progressBar = new SingleBar( + { format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums' }, + Presets.shades_classic, + ); + progressBar.start(newAlbums.size, 0); + + try { + for (const albumNames of chunk([...newAlbums], concurrency)) { + const items = await Promise.all( + albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } })), ); - console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`); - console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`); - } - - if (!options.delete) { - return; - } - - if (options.dryRun) { - console.log(`Would now have deleted assets, but skipped due to dry run`); - return; - } - - console.log('Deleting assets that have been uploaded...'); - - await this.deleteAssets(newAssets, options); - } - - public async checkAssets( - assetsToCheck: Asset[], - concurrency: number, - ): Promise<{ newAssets: Asset[]; duplicateAssets: Asset[]; rejectedAssets: Asset[] }> { - for (const assets of chunk(assetsToCheck, concurrency)) { - await Promise.all(assets.map((asset: Asset) => asset.prepare())); - } - - const checkProgress = new cliProgress.SingleBar( - { format: 'Checking assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, - cliProgress.Presets.shades_classic, - ); - checkProgress.start(assetsToCheck.length, 0); - - const newAssets = []; - const duplicateAssets = []; - const rejectedAssets = []; - try { - for (const assets of chunk(assetsToCheck, concurrency)) { - const checkedAssets = await this.getStatus(assets); - for (const checked of checkedAssets) { - if (checked.status === CheckResponseStatus.ACCEPT) { - newAssets.push(checked.asset); - } else if (checked.status === CheckResponseStatus.DUPLICATE) { - duplicateAssets.push(checked.asset); - } else { - rejectedAssets.push(checked.asset); - } - checkProgress.increment(); - } + for (const { id, albumName } of items) { + existingAlbums.set(albumName, id); } - } finally { - checkProgress.stop(); + progressBar.increment(albumNames.length); } - - return { newAssets, duplicateAssets, rejectedAssets }; + } finally { + progressBar.stop(); } - public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise { - let totalSize = 0; + console.log(`Successfully created ${newAlbums.size} new album${s(newAlbums.size)}`); + console.log(`Successfully updated ${assets.length} asset${s(assets.length)}`); - // Compute total size first - for (const asset of assetsToUpload) { - totalSize += asset.fileSize ?? 0; + const albumToAssets = new Map(); + for (const asset of assets) { + const albumName = getAlbumName(asset.filepath, options); + if (!albumName) { + continue; } - - if (options.dryRun) { - return totalSize; - } - - const uploadProgress = new cliProgress.SingleBar( - { - format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}', - }, - cliProgress.Presets.shades_classic, - ); - uploadProgress.start(totalSize, 0); - uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); - - let totalSizeUploaded = 0; - try { - for (const assets of chunk(assetsToUpload, options.concurrency)) { - const ids = await this.uploadAssets(assets); - for (const [asset, id] of zipDefined(assets, ids)) { - asset.id = id; - if (asset.fileSize) { - totalSizeUploaded += asset.fileSize ?? 0; - } else { - console.log(`Could not determine file size for ${asset.path}`); - } - } - uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) }); + const albumId = existingAlbums.get(albumName); + if (albumId) { + if (!albumToAssets.has(albumId)) { + albumToAssets.set(albumId, []); } - } finally { - uploadProgress.stop(); + albumToAssets.get(albumId)?.push(asset.id); } - - return totalSizeUploaded; } - public async getFiles(paths: string[], options: UploadOptionsDto): Promise { - const inputFiles: string[] = []; - for (const pathArgument of paths) { - const fileStat = await fs.promises.lstat(pathArgument); - if (fileStat.isFile()) { - inputFiles.push(pathArgument); + const albumUpdateProgress = new SingleBar( + { format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + albumUpdateProgress.start(assets.length, 0); + + try { + for (const [albumId, assets] of albumToAssets.entries()) { + for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) { + await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); + albumUpdateProgress.increment(assetBatch.length); } } - - const files: string[] = await this.crawl(paths, options); - files.push(...inputFiles); - return files; + } finally { + albumUpdateProgress.stop(); } +}; - public async getAlbums(): Promise> { - const existingAlbums = await getAllAlbums({}); - - const albumMapping = new Map(); - for (const album of existingAlbums) { - albumMapping.set(album.albumName, album.id); - } - - return albumMapping; - } - - public async updateAlbums( - assets: Asset[], - options: UploadOptionsDto, - ): Promise<{ createdAlbumCount: number; updatedAssetCount: number }> { - if (options.albumName) { - for (const asset of assets) { - asset.albumName = options.albumName; - } - } - - const existingAlbums = await this.getAlbums(); - const assetsToUpdate = assets.filter( - (asset): asset is Asset & { albumName: string; id: string } => !!(asset.albumName && asset.id), - ); - - const newAlbumsSet: Set = new Set(); - for (const asset of assetsToUpdate) { - if (!existingAlbums.has(asset.albumName)) { - newAlbumsSet.add(asset.albumName); - } - } - - const newAlbums = [...newAlbumsSet]; - - if (options.dryRun) { - return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length }; - } - - const albumCreationProgress = new cliProgress.SingleBar( - { - format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums', - }, - cliProgress.Presets.shades_classic, - ); - albumCreationProgress.start(newAlbums.length, 0); - - try { - for (const albumNames of chunk(newAlbums, options.concurrency)) { - const newAlbumIds = await Promise.all( - albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } }).then((r) => r.id)), - ); - - for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) { - existingAlbums.set(albumName, albumId); - } - - albumCreationProgress.increment(albumNames.length); - } - } finally { - albumCreationProgress.stop(); - } - - const albumToAssets = new Map(); - for (const asset of assetsToUpdate) { - const albumId = existingAlbums.get(asset.albumName); - if (albumId) { - if (!albumToAssets.has(albumId)) { - albumToAssets.set(albumId, []); - } - albumToAssets.get(albumId)?.push(asset.id); - } - } - - const albumUpdateProgress = new cliProgress.SingleBar( - { - format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets', - }, - cliProgress.Presets.shades_classic, - ); - albumUpdateProgress.start(assetsToUpdate.length, 0); - - try { - for (const [albumId, assets] of albumToAssets.entries()) { - for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) { - await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); - albumUpdateProgress.increment(assetBatch.length); - } - } - } finally { - albumUpdateProgress.stop(); - } - - return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length }; - } - - public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise { - const deletionProgress = new cliProgress.SingleBar( - { - format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets', - }, - cliProgress.Presets.shades_classic, - ); - deletionProgress.start(assets.length, 0); - - try { - for (const assetBatch of chunk(assets, options.concurrency)) { - await Promise.all(assetBatch.map((asset: Asset) => asset.delete())); - deletionProgress.update(assetBatch.length); - } - } finally { - deletionProgress.stop(); - } - } - - private async getStatus(assets: Asset[]): Promise<{ asset: Asset; status: CheckResponseStatus }[]> { - const checkResponse = await this.checkHashes(assets); - - const responses = []; - for (const [check, asset] of zipDefined(checkResponse, assets)) { - if (check.assetId) { - asset.id = check.assetId; - } - - if (check.action === 'accept') { - responses.push({ asset, status: CheckResponseStatus.ACCEPT }); - } else if (check.reason === 'duplicate') { - responses.push({ asset, status: CheckResponseStatus.DUPLICATE }); - } else { - responses.push({ asset, status: CheckResponseStatus.REJECT }); - } - } - - return responses; - } - - private async checkHashes(assetsToCheck: Asset[]): Promise { - const checksums = await Promise.all(assetsToCheck.map((asset) => asset.hash())); - const assetBulkUploadCheckDto = { - assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })), - }; - const checkResponse = await checkBulkUpload({ assetBulkUploadCheckDto }); - return checkResponse.results; - } - - private async uploadAssets(assets: Asset[]): Promise { - const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData())); - const results = await Promise.all(fileRequests.map((request) => this.uploadAsset(request))); - return results.map((response) => response.id); - } - - private async crawl(paths: string[], options: UploadOptionsDto): Promise { - const formatResponse = await getSupportedMediaTypes(); - const crawlService = new CrawlService(formatResponse.image, formatResponse.video); - - return crawlService.crawl({ - pathsToCrawl: paths, - recursive: options.recursive, - exclusionPatterns: options.exclusionPatterns, - includeHidden: options.includeHidden, - }); - } - - private async uploadAsset(data: FormData): Promise<{ id: string }> { - const { baseUrl, headers } = defaults; - - const response = await fetch(`${baseUrl}/asset/upload`, { - method: 'post', - redirect: 'error', - headers: headers as Record, - body: data, - }); - if (response.status !== 200 && response.status !== 201) { - throw new Error(await response.text()); - } - return response.json(); - } -} +const getAlbumName = (filepath: string, options: UploadOptionsDto) => { + const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2); + return options.albumName ?? folderName; +}; diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index 05f3d7953..6675201a7 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -3,12 +3,12 @@ import { existsSync } from 'node:fs'; import { mkdir, unlink } from 'node:fs/promises'; import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; -export const login = async (instanceUrl: string, apiKey: string, options: BaseOptions) => { - console.log(`Logging in to ${instanceUrl}`); +export const login = async (url: string, key: string, options: BaseOptions) => { + console.log(`Logging in to ${url}`); const { configDirectory: configDir } = options; - await connect(instanceUrl, apiKey); + await connect(url, key); const [error, userInfo] = await withError(getMyUserInfo()); if (error) { @@ -27,7 +27,7 @@ export const login = async (instanceUrl: string, apiKey: string, options: BaseOp } } - await writeAuthFile(configDir, { instanceUrl, apiKey }); + await writeAuthFile(configDir, { url, key }); console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`); }; diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts index a7de804df..074513bd6 100644 --- a/cli/src/commands/server-info.ts +++ b/cli/src/commands/server-info.ts @@ -1,15 +1,24 @@ -import { getAssetStatistics, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; +import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; import { BaseOptions, authenticate } from 'src/utils'; export const serverInfo = async (options: BaseOptions) => { - await authenticate(options); + const { url } = await authenticate(options); - const versionInfo = await getServerVersion(); - const mediaTypes = await getSupportedMediaTypes(); - const stats = await getAssetStatistics({}); + const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([ + getServerVersion(), + getSupportedMediaTypes(), + getAssetStatistics({}), + getMyUserInfo(), + ]); - console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); - console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`); - console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`); - console.log(`Statistics:\n Images: ${stats.images}\n Videos: ${stats.videos}\n Total: ${stats.total}`); + console.log(`Server Info (via ${userInfo.email})`); + console.log(` Url: ${url}`); + console.log(` Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); + console.log(` Formats:`); + console.log(` Images: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`); + console.log(` Videos: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`); + console.log(` Statistics:`); + console.log(` Images: ${stats.images}`); + console.log(` Videos: ${stats.videos}`); + console.log(` Total: ${stats.total}`); }; diff --git a/cli/src/index.ts b/cli/src/index.ts index bf7e13f44..c3da631b9 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -19,7 +19,7 @@ const program = new Command() .default(defaultConfigDirectory), ) .addOption(new Option('-u, --url [url]', 'Immich server URL').env('IMMICH_INSTANCE_URL')) - .addOption(new Option('-k, --key [apiKey]', 'Immich API key').env('IMMICH_API_KEY')); + .addOption(new Option('-k, --key [key]', 'Immich API key').env('IMMICH_API_KEY')); program .command('login') diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts deleted file mode 100644 index 3ad0fcf3b..000000000 --- a/cli/src/services/crawl.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { glob } from 'glob'; -import * as fs from 'node:fs'; - -export class CrawlOptions { - pathsToCrawl!: string[]; - recursive? = false; - includeHidden? = false; - exclusionPatterns?: string[]; -} - -export class CrawlService { - private readonly extensions!: string[]; - - constructor(image: string[], video: string[]) { - this.extensions = [...image, ...video].map((extension) => extension.replace('.', '')); - } - - async crawl(options: CrawlOptions): Promise { - const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options; - - if (!pathsToCrawl) { - return []; - } - - const patterns: string[] = []; - const crawledFiles: string[] = []; - - for await (const currentPath of pathsToCrawl) { - try { - const stats = await fs.promises.stat(currentPath); - if (stats.isFile() || stats.isSymbolicLink()) { - crawledFiles.push(currentPath); - } else { - patterns.push(currentPath); - } - } catch (error: any) { - if (error.code === 'ENOENT') { - patterns.push(currentPath); - } else { - throw error; - } - } - } - - let searchPattern: string; - if (patterns.length === 1) { - searchPattern = patterns[0]; - } else if (patterns.length === 0) { - return crawledFiles; - } else { - searchPattern = '{' + patterns.join(',') + '}'; - } - - if (recursive) { - searchPattern = searchPattern + '/**/'; - } - - searchPattern = `${searchPattern}/*.{${this.extensions.join(',')}}`; - - const globbedFiles = await glob(searchPattern, { - absolute: true, - nocase: true, - nodir: true, - dot: includeHidden, - ignore: exclusionPatterns, - }); - - return [...crawledFiles, ...globbedFiles].sort(); - } -} diff --git a/cli/src/services/crawl.service.spec.ts b/cli/src/utils.spec.ts similarity index 94% rename from cli/src/services/crawl.service.spec.ts rename to cli/src/utils.spec.ts index 93879f21e..9c2570279 100644 --- a/cli/src/services/crawl.service.spec.ts +++ b/cli/src/utils.spec.ts @@ -1,14 +1,31 @@ import mockfs from 'mock-fs'; -import { CrawlOptions, CrawlService } from './crawl.service'; +import { CrawlOptions, crawl } from 'src/utils'; interface Test { test: string; - options: CrawlOptions; + options: Omit; files: Record; } const cwd = process.cwd(); +const extensions = [ + '.jpg', + '.jpeg', + '.png', + '.heif', + '.heic', + '.tif', + '.nef', + '.webp', + '.tiff', + '.dng', + '.gif', + '.mov', + '.mp4', + '.webm', +]; + const tests: Test[] = [ { test: 'should return empty when crawling an empty path list', @@ -251,12 +268,7 @@ const tests: Test[] = [ }, ]; -describe(CrawlService.name, () => { - const sut = new CrawlService( - ['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'], - ['.mov', '.mp4', '.webm'], - ); - +describe('crawl', () => { afterEach(() => { mockfs.restore(); }); @@ -266,7 +278,7 @@ describe(CrawlService.name, () => { it(test, async () => { mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, '']))); - const actual = await sut.crawl(options); + const actual = await crawl({ ...options, extensions }); const expected = Object.entries(files) .filter((entry) => entry[1]) .map(([file]) => file); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index f99a0e66a..b2d34bbb4 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,54 +1,61 @@ import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk'; -import { readFile, writeFile } from 'node:fs/promises'; +import { glob } from 'glob'; +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { readFile, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import yaml from 'yaml'; export interface BaseOptions { configDirectory: string; - apiKey?: string; - instanceUrl?: string; + key?: string; + url?: string; } -export interface AuthDto { - instanceUrl: string; - apiKey: string; -} +export type AuthDto = { url: string; key: string }; +type OldAuthDto = { instanceUrl: string; apiKey: string }; -export const authenticate = async (options: BaseOptions): Promise => { - const { configDirectory: configDir, instanceUrl, apiKey } = options; +export const authenticate = async (options: BaseOptions): Promise => { + const { configDirectory: configDir, url, key } = options; // provided in command - if (instanceUrl && apiKey) { - await connect(instanceUrl, apiKey); - return; + if (url && key) { + return connect(url, key); } - // fallback to file + // fallback to auth file const config = await readAuthFile(configDir); - await connect(config.instanceUrl, config.apiKey); + const auth = await connect(config.url, config.key); + if (auth.url !== config.url) { + await writeAuthFile(configDir, auth); + } + + return auth; }; -export const connect = async (instanceUrl: string, apiKey: string): Promise => { - const wellKnownUrl = new URL('.well-known/immich', instanceUrl); +export const connect = async (url: string, key: string) => { + const wellKnownUrl = new URL('.well-known/immich', url); try { const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); - const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); - if (endpoint !== instanceUrl) { + const endpoint = new URL(wellKnown.api.endpoint, url).toString(); + if (endpoint !== url) { console.debug(`Discovered API at ${endpoint}`); } - instanceUrl = endpoint; + url = endpoint; } catch { // noop } - defaults.baseUrl = instanceUrl; - defaults.headers = { 'x-api-key': apiKey }; + defaults.baseUrl = url; + defaults.headers = { 'x-api-key': key }; const [error] = await withError(getMyUserInfo()); if (isHttpError(error)) { logError(error, 'Failed to connect to server'); process.exit(1); } + + return { url, key }; }; export const logError = (error: unknown, message: string) => { @@ -66,7 +73,12 @@ export const readAuthFile = async (dir: string) => { try { const data = await readFile(getAuthFilePath(dir)); // TODO add class-transform/validation - return yaml.parse(data.toString()) as AuthDto; + const auth = yaml.parse(data.toString()) as AuthDto | OldAuthDto; + const { instanceUrl, apiKey } = auth as OldAuthDto; + if (instanceUrl && apiKey) { + return { url: instanceUrl, key: apiKey }; + } + return auth as AuthDto; } catch (error: Error | any) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { console.log('No auth file exists. Please login first.'); @@ -87,3 +99,74 @@ export const withError = async (promise: Promise): Promise<[Error, undefin return [error, undefined]; } }; + +export interface CrawlOptions { + pathsToCrawl: string[]; + recursive?: boolean; + includeHidden?: boolean; + exclusionPatterns?: string[]; + extensions: string[]; +} +export const crawl = async (options: CrawlOptions): Promise => { + const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options; + const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', '')); + + if (pathsToCrawl.length === 0) { + return []; + } + + const patterns: string[] = []; + const crawledFiles: string[] = []; + + for await (const currentPath of pathsToCrawl) { + try { + const stats = await stat(currentPath); + if (stats.isFile() || stats.isSymbolicLink()) { + crawledFiles.push(currentPath); + } else { + patterns.push(currentPath); + } + } catch (error: any) { + if (error.code === 'ENOENT') { + patterns.push(currentPath); + } else { + throw error; + } + } + } + + let searchPattern: string; + if (patterns.length === 1) { + searchPattern = patterns[0]; + } else if (patterns.length === 0) { + return crawledFiles; + } else { + searchPattern = '{' + patterns.join(',') + '}'; + } + + if (recursive) { + searchPattern = searchPattern + '/**/'; + } + + searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`; + + const globbedFiles = await glob(searchPattern, { + absolute: true, + nocase: true, + nodir: true, + dot: includeHidden, + ignore: exclusionPatterns, + }); + + return [...crawledFiles, ...globbedFiles].sort(); +}; + +export const sha1 = (filepath: string) => { + const hash = createHash('sha1'); + return new Promise((resolve, reject) => { + const rs = createReadStream(filepath); + rs.on('error', reject); + rs.on('data', (chunk) => hash.update(chunk)); + rs.on('end', () => resolve(hash.digest('hex'))); + }); +}; diff --git a/design/immich-screenshots.png b/design/immich-screenshots.png index 1db7cab76..6123279f2 100644 Binary files a/design/immich-screenshots.png and b/design/immich-screenshots.png differ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 2541a1a20..8b85b0028 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -2,8 +2,6 @@ # - https://immich.app/docs/developer/setup # - https://immich.app/docs/developer/troubleshooting -version: '3.8' - name: immich-dev x-server-build: &server-common @@ -99,7 +97,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 database: container_name: immich_postgres diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 77effc15f..1bcb5e327 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,5 +1,3 @@ -version: '3.8' - name: immich-prod x-server-build: &server-common @@ -56,7 +54,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 restart: always database: @@ -78,7 +76,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:bc1794e85c9e00293351b967efa267ce6af1c824ac875a9d0c7ac84700a8b53e + image: prom/prometheus@sha256:dec2018ae55885fed717f25c289b8c9cff0bf5fbb9e619fb49b6161ac493c016 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -90,7 +88,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:10.4.0-ubuntu@sha256:c1f582b7cc4c1b9805d187b5600ce7879550a12ef6d29571da133c3d3fc67a9c + image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c volumes: - grafana-data:/var/lib/grafana diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 46b4a44a8..7f27798c5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - # # WARNING: Make sure to use the docker-compose.yml of the current release: # diff --git a/docs/blog/2022/11-10/release-1.36.mdx b/docs/blog/2022/11-10/release-1.36.mdx index 9980c1a05..5f5643196 100644 --- a/docs/blog/2022/11-10/release-1.36.mdx +++ b/docs/blog/2022/11-10/release-1.36.mdx @@ -10,8 +10,8 @@ Hello everyone, it is my pleasure to deliver the new release of Immich to you. T Some notable features are: -- [OAuth integration](#livephoto-ios-support-) -- [LivePhoto support on iOS](#oauth-integration-) +- OAuth integration +- LivePhoto support on iOS - User config system diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 1465cc9fd..98b3db996 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -253,8 +253,19 @@ The initial backup is the most intensive due to the number of jobs running. The ### Can I limit the amount of CPU and RAM usage? -By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. -You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to limit this. +By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources. + +```yaml +deploy: + resources: + limits: + # Number of CPU threads + cpus: '1.00' + # Gigabytes of memory + memory: '1G' +``` + +For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit). ### How can I boost machine learning speed? @@ -288,10 +299,25 @@ Immich components are typically deployed using docker. To see logs for deployed ### How can I run Immich as a non-root user? You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. -You may need to add an additional volume to `immich-microservices` that mounts internally to `/usr/src/app/.reverse-geocoding-dump`. +You may need to add mount points or docker volumes for the following internal container paths: + +- `immich-machine-learning:/.config` +- `immich-machine-learning:/.cache` +- `redis:/data` The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`. +For a further hardened system, you can add the following block to every container except for `immich_postgres`. + +```yaml +security_opt: + # Prevent escalation of privileges after container is started + - no-new-privileges:true +cap_drop: + # Prevent access to raw network traffic + - NET_RAW +``` + ### How can I **purge** data from Immich? Data for Immich comes in two forms: diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 6d7ee6f15..b5e407169 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -20,7 +20,7 @@ The recommended way to backup and restore the Immich database is to use the `pg_ -```bash title='Bash' +```bash title='Backup' docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz" ``` diff --git a/docs/docs/administration/img/repair-page-1.png b/docs/docs/administration/img/repair-page-1.png new file mode 100644 index 000000000..4c6a1cddd Binary files /dev/null and b/docs/docs/administration/img/repair-page-1.png differ diff --git a/docs/docs/administration/img/repair-page.png b/docs/docs/administration/img/repair-page.png new file mode 100644 index 000000000..b6ae7e40b Binary files /dev/null and b/docs/docs/administration/img/repair-page.png differ diff --git a/docs/docs/administration/img/server-stats.png b/docs/docs/administration/img/server-stats.png new file mode 100644 index 000000000..0d9adf276 Binary files /dev/null and b/docs/docs/administration/img/server-stats.png differ diff --git a/docs/docs/administration/jobs.md b/docs/docs/administration/jobs.md index e02bfcaed..9c8536cfa 100644 --- a/docs/docs/administration/jobs.md +++ b/docs/docs/administration/jobs.md @@ -1,9 +1,13 @@ # Jobs -Several Immich functionalities are implemented as jobs, which run in the background. To view the status of a job navigate to the Administration Screen, and then the `Jobs` page. +The `immich-server` responds to API requests for data and files for the web and mobile app. To do this quickly and reliably, it offloads most other work to `immich-microservices` in the form of _jobs_. Simply put, a job is a request to process data in the background. Jobs are picked up automatically by microservices containers. -![Admin jobs](./img/admin-jobs.png) +When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page. + +Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed. :::info Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library. ::: + + diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index f97f8da7d..b273f2771 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -67,14 +67,20 @@ Once you have a new OAuth client application configured, Immich can be configure | Client Secret | string | (required) | Required. Client Secret (previous step) | | Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) | | Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | -| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label | -| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage | +| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** | +| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** | | Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) | | Button Text | string | Login with OAuth | Text for the OAuth button on the web | | Auto Register | boolean | true | When true, will automatically register a user the first time they sign in | | [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process | | [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI | +:::note Claim Options [1] + +Claim is only used on user creation and not synchronized after that. + +::: + :::info The Issuer URL should look something like the following, and return a valid json document. diff --git a/docs/docs/administration/password-login.md b/docs/docs/administration/password-login.md deleted file mode 100644 index ff4852eee..000000000 --- a/docs/docs/administration/password-login.md +++ /dev/null @@ -1,32 +0,0 @@ -# Password Login - -An overview of password login and related settings for Immich. - -## Enable/Disable - -Immich supports password login, which is enabled by default. The preferred way to disable it is via the [Administration Page](#administration-page), although it can also be changed via a [Server Command](#server-command) as well. - -### Administration Page - -To toggle the password login setting via the web, navigate to the "Administration", expand "Password Authentication", toggle the "Enabled" switch, and press "Save". - -![Password Login Settings](./img/password-login-settings.png) - -### Server Command - -There are two [Server Commands](/docs/administration/server-commands.md) for password login: - -1. `enable-password-login` -2. `disable-password-login` - -See [Server Commands](/docs/administration/server-commands.md) for more details about how to run them. - -## Password Reset - -### Admin - -To reset the administrator password, use the `reset-admin-password` [Server Command](/docs/administration/server-commands.md). - -### User - -Immich does not currently support self-service password reset. However, the administration can reset passwords for other users. See [User Management: Password Reset](/docs/administration/user-management.mdx#password-reset) for more information about how to do this. diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md new file mode 100644 index 000000000..c29aa54e6 --- /dev/null +++ b/docs/docs/administration/postgres-standalone.md @@ -0,0 +1,54 @@ +# Pre-existing Postgres + +While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container. + +By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich. + +:::tip +Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman. +::: + +## Prerequisites + +You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`. + +:::note +Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`. +::: + +## Specifying the connection URL + +You can connect to your pre-existing Postgres server by setting the `DB_URL` environment variable in the `.env` file. + +``` +DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename' + +# require a SSL connection to Postgres +# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require' + +# require a SSL connection, but don't enforce checking the certificate name +# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify' +``` + +## Without superuser permissions + +### Initial installation + +Immich can run without superuser permissions by following the below instructions at the `psql` prompt to prepare the database. + +```sql title="Set up Postgres for Immich" +CREATE DATABASE ; +\c +BEGIN; +ALTER DATABASE OWNER TO ; +CREATE EXTENSION vectors; +CREATE EXTENSION earthdistance CASCADE; +ALTER DATABASE SET search_path TO "$user", public, vectors; +GRANT USAGE ON SCHEMA vectors TO ; +ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO ; +COMMIT; +``` + +### Updating pgvecto.rs + +When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`. diff --git a/docs/docs/administration/repair-page.md b/docs/docs/administration/repair-page.md new file mode 100644 index 000000000..3c643ee71 --- /dev/null +++ b/docs/docs/administration/repair-page.md @@ -0,0 +1,31 @@ +# Repair Page + +The repair page is designed to give information to the system administrator about files that are not tracked, or offline paths. + +## Natural State + +In this situation, everything is in its place and there is no problem that the system administrator should be aware of. + + + +## Any Other Situation + +:::note RAM Usage +Several users report a situation where the page fails to load. In order to solve this problem you should try to allocate more RAM to Immich, if the problem continues, you should stop using the reverse proxy while loading the page. +::: + +In any other situation, there are 3 different options that can appear: + +- MATCHES - These files are matched by their checksums. + +- OFFLINE PATHS - These files are the result of manually deleting files in the upload library or a failed file move in the past (losing track of a file). + +:::tip +To get rid of Offline paths you can follow this [guide](/docs/guides/remove-offline-files.md) +::: + +- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug. + +In addition, you can download the information from a page, mark everything (in order to check hashing) and correct the problem if a match is found in the hashing. + + diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 24919347f..1d2488f11 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -1,29 +1,41 @@ # Reverse Proxy -Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. +Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Real-IP`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. + +:::note +The Repair page can take a long time to load. To avoid server timeouts or errors, we recommend specifying a timeout of at least 10 minutes on your proxy server. +::: ### Nginx example config -Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`. +Below is an example config for nginx. Make sure to set `public_url` to the front-facing URL of your instance, and `backend_url` to the path of the Immich server. ```nginx server { - server_name + server_name ; + # allow large file uploads client_max_body_size 50000M; - location / { - proxy_pass http://:2283; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + # Set headers + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; - # http://nginx.org/en/docs/http/websocket.html - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_redirect off; + # enable websockets: http://nginx.org/en/docs/http/websocket.html + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + + # set timeout + proxy_read_timeout 600s; + proxy_send_timeout 600s; + send_timeout 600s; + + location / { + proxy_pass http://:2283; } } ``` @@ -42,15 +54,13 @@ immich.example.org { Below is an example config for Apache2 site configuration. -``` +```ApacheConf ServerName ProxyRequests Off + # set timeout in seconds ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket ProxyPassReverse / http://127.0.0.1:2283/ ProxyPreserveHost On - ``` - -**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error. diff --git a/docs/docs/administration/server-stats.md b/docs/docs/administration/server-stats.md new file mode 100644 index 000000000..61f4d2d00 --- /dev/null +++ b/docs/docs/administration/server-stats.md @@ -0,0 +1,13 @@ +# Server Stats + +Server statistics to show the total number of videos, photos, and usage per user. + +:::info +If a storage quota has been defined for the user, the usage number will be displayed as a percentage of the total storage quota allocated to him. +::: + +:::info External library +External library is not included in the storage quota. +::: + + diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md new file mode 100644 index 000000000..21eeaaee8 --- /dev/null +++ b/docs/docs/administration/system-settings.md @@ -0,0 +1,173 @@ +# System Settings + +On the system settings page, the administrator can manage global settings for the Immich instance. + +:::note +Viewing and modifying the system settings is restricted to the Administrator. +::: + +:::tip +You can always return to the default settings by clicking the `Reset to default` button. +::: + +## Job Settings + +Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities. + +With higher concurrency, the host will work on more assets in parallel, +this advice improves throughput, not latency, for example, it will make Smart Search jobs process more quickly, but it won't make searching faster. + +It is important to remember that jobs like Smart Search, Face Detection, Facial Recognition, and Transcode Videos require a **lot** of processing power and therefore do not exaggerate the amount of jobs because you're probably thoroughly overloading the server. + +:::info Facial Recognition Concurrency +The Facial Recognition Concurrency value cannot be changed because +[DBSCAN](https://www.youtube.com/watch?v=RDZUdRSDOok) is traditionally sequential, but there are parallel implementations of it out there. Our implementation isn't parallel. +::: + +## External Library + +### Library watching (EXPERIMENTAL) + +External libraries can automatically import changed files without a full rescan. It will import the file whenever the operating system reports a file change. If your photos are mounted over the network, this does not work. + +### Periodic Scanning + +You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library. +You can set the scanning interval using the preset or cron format. For more information please refer to e.g. [Crontab Guru](https://crontab.guru/). + +## Logging + +By default logs are set to record at the log level, the network administrator can choose a deeper or lower level of logs according to his decision or according to the needs required by the Immich support team. + +Here you can [learn about the different error levels](https://sematext.com/blog/logging-levels/). + +## Machine Learning Settings + +Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters +You can choose to disable a certain type of machine learning, for example smart search or facial recognition. + +### Smart Search + +The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the +Smart Search job on all images to fully apply the change. + +:::info Internet connection +Changing models requires a connection to the Internet to download the model. +After downloading, there is no need for Immich to connect to the network +Unless version checking has been enabled in the settings. +::: + +### Facial Recognition + +Under these settings, you can change the facial recognition settings +Editable settings: + +- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model. +- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives. +- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible. +- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person. + +:::info +When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces. +You will have to restart **only** the job FACIAL RECOGNITION - ALL. + +If you replace the Facial Recognition Model, you will have to run the job FACE DETECTION - ALL. +::: + +:::tip identical twins +If you have twins, you might want to lower the Max Recognition Distance value, decreasing this a **bit** can make it distinguish between them. +::: + +## Map & GPS Settings + +### Map Settings + +In these settings, you can change the appearance of the map in night and day modes according to your personal preference and according to the supported options. +The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for example. + +### Reverse Geocoding Settings + +Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database. + +## OAuth Authentication + +Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth). + +## Password Authentication + +The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts. + +:::tip +You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login. +::: + +## Server Settings + +### External Domain + +When set, will override the domain name used when viewing and copying a shared link. + +### Welcome Message + +The administrator can set a custom message on the login screen (the message will be displayed to all users). + +## Storage Template + +Immich supports a custom [Storage Template](/docs/administration/storage-template). Learn more about this feature and its configuration [here](/docs/administration/storage-template). + +## Theme Settings + +You can write custom CSS that will get loaded in the web application for all users. This enables administrators to change fonts, colors, and other styles. + +For example: + +```css title='Custom CSS' +p { + color: green; +} +``` + +## Thumbnail Settings + +By default Immich creates 3 thumbnails for each asset, +Blurred (thumbhash) , Small (webp) , and Large (jpeg), using these settings you can change the quality for the thumbnail files that are created. + +**Small thumbnail resolution** +Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness. + +**Large thumbnail resolution** +Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness. + +**Quality** +Thumbnail quality from 1-100. Higher is better for quality but produces larger files. + +**Prefer wide gamut** +Use display p3 for thumbnails. This better preserves the vibrance of images with wide color spaces, but images may appear differently on old devices with an old browser version. Srgb images are kept as srgb to avoid color shifts. + +:::tip +The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space. +::: + +## Trash Settings + +In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator. + +The trash can be disabled, however this is not recommended as future files that are deleted will be permanently deleted. + +:::tip Keyboard shortcut for permanently deletion +You can select assets and press Ctrl + Del from the timeline for quick permanent deletion without the trash option. +::: + +## User Settings + +### Delete delay + +The system administrator can choose to delete users through the administration panel, the system administrator can delete users immediately or alternatively delay the deletion for users (7 days by default) this action permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution. + +## Version Check + +When this option is enabled the `immich-server` will periodically make requests to GitHub to check for new releases. + +## Video Transcoding Settings + +The system administrator can define parameters according to which video files will be converted to different formats (depending on the settings). The settings can be changed in depth, to learn more about the terminology used here, refer to FFmpeg documentation for [H.264](https://trac.ffmpeg.org/wiki/Encode/H.264) codec, [HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) codec and [VP9](https://trac.ffmpeg.org/wiki/Encode/VP9) codec. diff --git a/docs/docs/developer/database-migrations.md b/docs/docs/developer/database-migrations.md index a78274a61..2cddf5f38 100644 --- a/docs/docs/developer/database-migrations.md +++ b/docs/docs/developer/database-migrations.md @@ -1,14 +1,14 @@ # Database Migrations -After making any changes in the `server/src/infra/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration. +After making any changes in the `server/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration. 1. Run the command ```bash -npm run typeorm:migrations:generate ./src/infra/ +npm run typeorm:migrations:generate ``` 2. Check if the migration file makes sense. -3. Move the migration file to folder `./server/src/infra/migrations` in your code editor. +3. Move the migration file to folder `./server/src/migrations` in your code editor. The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately. diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index dc61c37d4..d553ca7d8 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -18,12 +18,11 @@ Thanks for being interested in contributing 😊 ### Server and web app -This environment includes the following services: +This environment includes the services below. Additional details are available in each service's README. -- Core server - `/server/src/immich` -- Machine learning - `/machine-learning` -- Microservices - `/server/src/microservicess` -- Web app - `/web` +- Server - [`/server`](https://github.com/immich-app/immich/tree/main/server) +- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web) +- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning) - Redis - PostgreSQL development database with exposed port `5432` so you can use any database client to acess it diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index 360b3c372..094de609f 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -1,6 +1,6 @@ # The Immich CLI -Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli). +Immich has a command line interface (CLI) that allows you to perform certain actions from the command line. ## Features @@ -54,16 +54,19 @@ Usage: immich [options] [command] Command line interface for Immich Options: - -V, --version output the version number - -d, --config Configuration directory (env: IMMICH_CONFIG_DIR) - -h, --help display help for command + -V, --version output the version number + -d, --config-directory Configuration directory where auth.yml will be stored (default: "~/.config/immich/", env: + IMMICH_CONFIG_DIR) + -u, --url [url] Immich server URL (env: IMMICH_INSTANCE_URL) + -k, --key [key] Immich API key (env: IMMICH_API_KEY) + -h, --help display help for command Commands: - upload [options] [paths...] Upload assets - server-info Display server information - login-key [instanceUrl] [apiKey] Login using an API key - logout Remove stored credentials - help [command] display help for command + login|login-key Login using an API key + logout Remove stored credentials + server-info Display server information + upload [options] [paths...] Upload assets + help [command] display help for command ``` ## Commands @@ -71,23 +74,24 @@ Commands: The upload command supports the following options: ``` -Usage: immich upload [options] [paths...] +Usage: immich upload [paths...] [options] Upload assets Arguments: - paths One or more paths to assets to be uploaded + paths One or more paths to assets to be uploaded Options: - -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) - -i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS) - -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) - -H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) - -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) - -A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) - -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) - --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) - --help display help for command + -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) + -i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS) + -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) + -H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) + -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) + -A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) + -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) + -c, --concurrency Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY) + --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) + --help display help for command ``` Note that the above options can read from environment variables as well. @@ -97,13 +101,13 @@ Note that the above options can read from environment variables as well. You begin by authenticating to your Immich server. ```bash -immich login-key [instanceUrl] [apiKey] +immich login [url] [key] ``` For instance, ```bash -immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG +immich login http://192.168.1.216:2283/api HFEJ38DNSDUEG ``` This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually. diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md new file mode 100644 index 000000000..0ea1382c8 --- /dev/null +++ b/docs/docs/features/monitoring.md @@ -0,0 +1,113 @@ +# Monitoring + +## Overview + +Immich provides a variety of performance metrics to allow for local monitoring and insights. This integration is primarily in the form of Prometheus metrics. However, exporting traces is also possible due to the use of OpenTelemetry instrumentation. + +:::note +This is an opt-in feature intended for you to monitor immich's performance. This data isn't sent anywhere beyond what you've configured. +::: + +## Prometheus + +Prometheus is a tool that collects metrics from a number of sources you configure. It operates in a "pull" strategy - that is, it periodically requests metrics from each defined source. This means that the source doesn't send anything until it's requested. It also means that the source -- immich, in this case -- has to expose an endpoint for Prometheus to target when it requests metrics. + +### Metrics + +These metrics come in a variety of forms: + +- Counters, which can only increase. Example: the number of times an endpoint has been called. +- Gauges, which can increase or decrease within a certain range. Example: CPU utilization. +- Histograms, where each observation is assigned to a certain number of "buckets". Example: response time, where each bucket is a number of milliseconds. This one is a bit more complicated. + - Buckets in this case are _cumulative_; that is, an observation is placed not only into the smallest bucket that contains it, but also to all buckets larger than this. For example, if a histogram has three buckets for 1ms, 5ms and 10ms, an observation of 3ms will be bucketed into both 5ms and 10ms. + +The metrics in immich are grouped into API (endpoint calls and response times), host (memory and CPU utilization), and IO (internal database queries, image processing, and so on). Each group of metrics can be enabled or disabled independently. + +### Configuration + +Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable. + +:::tip +`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics. +::: + +The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way. + +You can start by defining a Prometheus service in the Compose file: + +```yaml +immich-prometheus: + container_name: immich_prometheus + ports: + # this exposes the default port for Prometheus so you can interact with it + - 9090:9090 + image: prom/prometheus + volumes: + # the Prometheus configuration file - a barebones one is provided to get started + - ./prometheus.yml:/etc/prometheus/prometheus.yml + # a named volume defined in the bottom of the Compose file; it can also be a mounted folder + - prometheus-data:/prometheus +``` + +You will also need to add `prometheus-data` to the list of volumes in the bottom of the Compose file: + +```yaml +volumes: + model-cache: + prometheus-data: +``` + +The last piece is the [configuration file][prom-file]. This file defines (among other things) the sources Prometheus should target. Download it and place it in the same folder as the Compose file. + +:::tip +The provided file is just a starting point. There are a ton of ways to configure Prometheus, so feel free to experiment! +::: + +After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network. + +:::note +To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8081` to the microservices container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects. +::: + +### Usage + +So after setting up Prometheus, how do you actually view the metrics? The simplest way is to use Prometheus directly. Visiting Prometheus will show you a web UI where you can search for and visualize metrics. You can also view the status of your data sources and configure settings, but this is beyond the scope of this guide. + +## Grafana + +For a dedicated tool with nice presentation, you can use Grafana instead. This connects to Prometheus (and possibly other sources) for sophisticated data visualization. + +Setting up Grafana is similar to Prometheus. You can add a service for it: + +```yaml +immich-grafana: + container_name: immich_grafana + command: ['./run.sh', '-disable-reporting'] # this is to disable Grafana's telemetry + ports: + - 3000:3000 + image: grafana/grafana + volumes: + # stores your pretty dashboards and panels + - grafana-data:/var/lib/grafana +``` + +And add another volume for it: + +```yaml +volumes: + model-cache: + prometheus-data: + grafana-data: +``` + +After bringing down the services and back up again, you can now visit Grafana to view your metrics. On the first login, enter `admin` for both username and password and update your password. You can then go to the settings and add a data source with `http://immich-prometheus:9090` to point Grafana to your Prometheus instance. + +### Usage + +You can make your first dashboard to get started. Don't forget to save it frequently, or you'll lose all your progress! + +You can then make a new panel, specifying Prometheus as the data source for it. + +-- TODO: add images and more details here + +[prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md new file mode 100644 index 000000000..a2dc56b66 --- /dev/null +++ b/docs/docs/features/supported-formats.md @@ -0,0 +1,42 @@ +# Supported formats + +Immich supports a number of image and video formats, the most common of which are outlined here. + +:::note +For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts). +::: + +## Image formats + +| Format | Extension(s) | Supported? | Notes | +| :-------- | :---------------------------- | :----------------: | :-------------- | +| `AVIF` | `.avif` | :white_check_mark: | | +| `BMP` | `.bmp` | :white_check_mark: | | +| `GIF` | `.gif` | :white_check_mark: | | +| `HEIC` | `.heic` | :white_check_mark: | | +| `HEIF` | `.heif` | :white_check_mark: | | +| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | | +| `JPEG XL` | `.jxl` | :white_check_mark: | | +| `PNG` | `.png` | :white_check_mark: | | +| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | +| `RAW` | `.raw` | :white_check_mark: | | +| `RW2` | `.rw2` | :white_check_mark: | | +| `SVG` | `.svg` | :white_check_mark: | | +| `TIFF` | `.tif` `.tiff` | :white_check_mark: | | +| `WEBP` | `.webp` | :white_check_mark: | | + +## Video formats + +| Format | Extension(s) | Supported? | Notes | +| :---------- | :-------------------- | :----------------: | :---- | +| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | | +| `AVI` | `.avi` | :white_check_mark: | | +| `FLV` | `.flv` | :white_check_mark: | | +| `M4V` | `.m4v` | :white_check_mark: | | +| `MATROSKA` | `.mkv` | :white_check_mark: | | +| `MP2T` | `.mts` `.m2ts` | :white_check_mark: | | +| `MP4` | `.mp4` `.insv` | :white_check_mark: | | +| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | +| `QUICKTIME` | `.mov` | :white_check_mark: | | +| `WEBM` | `.webm` | :white_check_mark: | | +| `WMV` | `.wmv` | :white_check_mark: | | diff --git a/docs/docs/features/xmp-sidecars.md b/docs/docs/features/xmp-sidecars.md index d184ce227..371547aa7 100644 --- a/docs/docs/features/xmp-sidecars.md +++ b/docs/docs/features/xmp-sidecars.md @@ -6,7 +6,7 @@ Immich can ingest XMP sidecars on file upload (via the CLI) as well as detect ne XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the media file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary. -When importing files via the CLI bulk uploader, Immich will automatically detect XMP sidecar files as files that exist next to the original media file and have the exact same name with an additional `.xmp` file extension (i.e., `PXL_20230401_203352928.MP.jpg` and `PXL_20230401_203352928.MP.jpg.xmp`). +When importing files via the CLI bulk uploader or parsing photo metadata for external libraries, Immich will automatically detect XMP sidecar files as files that exist next to the original media file. Immich will look files that have the same name as the photo, but with the `.xmp` file extension. The same name can either include the photo's file extension or without the photo's file extension. For example, for a photo named `PXL_20230401_203352928.MP.jpg`, Immich will look for an XMP file named either `PXL_20230401_203352928.MP.jpg.xmp` or `PXL_20230401_203352928.MP.xmp`. If both `PXL_20230401_203352928.MP.jpg.xmp` and `PXL_20230401_203352928.MP.xmp` are present, Immich will prefer `PXL_20230401_203352928.MP.jpg.xmp`. There are 2 administrator jobs associated with sidecar files: `SYNC` and `DISCOVER`. The sync job will re-scan all media with existing sidecar files and queue them for a metadata refresh. This is a great use case when third-party applications are used to modify the metadata of media. The discover job will attempt to scan the filesystem for new sidecar files for all media that does not currently have a sidecar file associated with it. diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index fe369f899..e8252f25d 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -45,7 +45,7 @@ SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "ex ``` ```sql title="Without thumbnails" -SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL; +SELECT * FROM "assets" WHERE "assets"."previewPath" IS NULL OR "assets"."thumbnailPath" IS NULL; ``` ```sql title="By type" diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index fef680d26..766318d5a 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -56,4 +56,4 @@ A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn ### Cons - Complex configuration -- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out. +- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active development and the existence of severe security vulnerabilities cannot be ruled out. diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index b7865686c..087f9aab7 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -16,7 +16,12 @@ version: '3.8' services: immich-machine-learning: container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.ml.yml + # service: # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable volumes: - model-cache:/cache restart: always diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md index cd43d660b..9a4f6c529 100644 --- a/docs/docs/guides/template-backup-script.md +++ b/docs/docs/guides/template-backup-script.md @@ -9,8 +9,8 @@ The database is saved to your Immich upload folder in the `database-backup` subd ### Prerequisites - Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html). -- To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/). -- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. +- (Optional) To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/). +- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. If you skipped the previous step, make sure this step is done from your root account. To initialize the borg repository, run the following commands once. @@ -19,16 +19,13 @@ UPLOAD_LOCATION="/path/to/immich/directory" # Immich database location, as BACKUP_PATH="/path/to/local/backup/directory" mkdir "$UPLOAD_LOCATION/database-backup" -mkdir "$BACKUP_PATH/immich-borg" - borg init --encryption=none "$BACKUP_PATH/immich-borg" ## Remote set up REMOTE_HOST="remote_host@IP" REMOTE_BACKUP_PATH="/path/to/remote/backup/directory" -ssh "$REMOTE_HOST" "mkdir \"$REMOTE_BACKUP_PATH\"/immich-borg" -ssh "$REMOTE_HOST" "borg init --encryption=none \"$REMOTE_BACKUP_PATH\"/immich-borg" +borg init --encryption=none "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg" ``` Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no `:`, `@`, or `"` characters in your paths. If these characters exist, you will need to escape and/or rename the paths. diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 9a1d1acb1..a890d674b 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -114,9 +114,11 @@ The default configuration looks like this: "hashVerificationEnabled": true, "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" }, - "thumbnail": { - "webpSize": 250, - "jpegSize": 1440, + "image": { + "thumbnailFormat": "webp", + "thumbnailSize": 250, + "previewFormat": "jpeg", + "previewSize": 1440, "quality": 80, "colorspace": "p3" }, diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index ca86c52d6..3a270c21b 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -21,7 +21,7 @@ cd ./immich-app Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file], either by running the following commands: ```bash title="Get docker-compose.yml file" -wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml ``` ```bash title="Get .env file" @@ -29,11 +29,11 @@ wget -O .env https://github.com/immich-app/immich/releases/latest/download/examp ``` ```bash title="(Optional) Get hwaccel.transcoding.yml file" -wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml +wget -O hwaccel.transcoding.yml https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml ``` ```bash title="(Optional) Get hwaccel.ml.yml file" -wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml +wget -O hwaccel.ml.yml https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml ``` or by downloading from your browser and moving the files to the directory that you created. diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 9fc1b20d2..5f727999e 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -41,11 +41,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices | :::tip +`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`. -`TZ` is only used by the `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. - -`exiftool` is only present in the microservices container. - +`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata. ::: ## Ports @@ -147,6 +145,18 @@ Other machine learning parameters can be tuned from the admin UI. ::: +## Prometheus + +| Variable | Description | Default | Services | +| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- | +| `IMMICH_METRICS`\*1 | Toggle all metrics (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices | + +\*1: Overridden for a metric group when its corresponding environmental variable is set. + ## Docker Secrets The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security. @@ -154,13 +164,14 @@ The following variables support the use of [Docker secrets](https://docs.docker. To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of the `_FILE` variable should be set to the path of a file containing the variable value. -| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable | -| :----------------: | :-----------------------------------------: | -| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`\*1 | -| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`\*1 | -| `DB_USERNAME` | `DB_USERNAME_FILE`\*1 | -| `DB_PASSWORD` | `DB_PASSWORD_FILE`\*1 | -| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`\*2 | +| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable | +| :----------------- | :------------------------------------------ | +| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`\*1 | +| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`\*1 | +| `DB_USERNAME` | `DB_USERNAME_FILE`\*1 | +| `DB_PASSWORD` | `DB_PASSWORD_FILE`\*1 | +| `DB_URL` | `DB_URL_FILE`\*1 | +| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`\*2 | \*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for details on how to use Docker Secrets in the Postgres image. diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index cf9313042..b52fd4649 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -1,7 +1,7 @@ Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level. :::note new version -On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further). +On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further). ::: :::tip diff --git a/docs/package-lock.json b/docs/package-lock.json index 9fbcc56a4..d86f69ac9 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -3429,9 +3429,9 @@ } }, "node_modules/@tsconfig/docusaurus": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.2.tgz", - "integrity": "sha512-12HWfYmgUl4M2o76/TFufGtI68wl2k/b8qPrIrG7ci9YJLrpAtadpy897Bz5v29Mlkr7a1Hq4KHdQTKtU+2rhQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.3.tgz", + "integrity": "sha512-3l1L5PzWVa7l0691TjnsZ0yOIEwG9DziSqu5IPZPlI5Dowi7z42cEym8Y35GHbgHvPcBfNxfrbxm7Cncn4nByQ==", "dev": true }, "node_modules/@types/acorn": { @@ -4264,9 +4264,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "funding": [ { "type": "opencollective", @@ -4283,7 +4283,7 @@ ], "dependencies": { "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -4728,9 +4728,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "version": "1.0.30001600", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", + "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", "funding": [ { "type": "opencollective", @@ -12691,9 +12691,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -12711,7 +12711,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -15295,9 +15295,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -15781,9 +15781,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15793,7 +15793,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -16141,9 +16141,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/docs/static/_redirects b/docs/static/_redirects index b12fee69a..5f2d5c5a1 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -24,3 +24,4 @@ /docs/features/user-management /docs/administration/user-management 301 /docs/developer/contributing /docs/developer/pr-checklist 301 /docs/guides/machine-learning /docs/guides/remote-machine-learning 301 +/docs/administration/password-login /docs/administration/system-settings 301 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 7cda6a12c..e10f22156 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -36,7 +36,7 @@ services: <<: *server-common redis: - image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 1b6d8ad19..894029102 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.98.2", + "version": "1.100.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -38,7 +38,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.1.0", + "version": "2.2.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -80,7 +80,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -339,9 +339,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -355,9 +355,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -371,9 +371,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -387,9 +387,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -403,9 +403,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -419,9 +419,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -435,9 +435,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -451,9 +451,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -467,9 +467,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -483,9 +483,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -499,9 +499,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -515,9 +515,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -547,9 +547,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -563,9 +563,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -579,9 +579,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -595,9 +595,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -611,9 +611,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -627,9 +627,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -643,9 +643,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -659,9 +659,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -675,9 +675,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -691,9 +691,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -863,9 +863,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -941,9 +941,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", - "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", "cpu": [ "arm" ], @@ -954,9 +954,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", - "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", "cpu": [ "arm64" ], @@ -967,9 +967,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", - "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ "arm64" ], @@ -980,9 +980,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", - "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", "cpu": [ "x64" ], @@ -993,9 +993,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", - "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", "cpu": [ "arm" ], @@ -1006,9 +1006,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", - "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", "cpu": [ "arm64" ], @@ -1019,9 +1019,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", - "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", "cpu": [ "arm64" ], @@ -1032,9 +1032,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", - "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", "cpu": [ "riscv64" ], @@ -1045,9 +1045,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", - "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", "cpu": [ "x64" ], @@ -1058,9 +1058,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", - "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", "cpu": [ "x64" ], @@ -1071,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", - "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", "cpu": [ "arm64" ], @@ -1084,9 +1084,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", - "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", "cpu": [ "ia32" ], @@ -1097,9 +1097,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", - "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -1158,9 +1158,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", - "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1173,9 +1173,9 @@ "dev": true }, "node_modules/@types/pg": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.2.tgz", - "integrity": "sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==", + "version": "8.11.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.4.tgz", + "integrity": "sha512-yw3Bwbda6vO+NvI1Ue/YKOwtl31AYvvd/e73O3V4ZkNzuGpTDndLSyc0dQRB2xrQqDePd20pEGIfqSp/GH3pRw==", "dev": true, "dependencies": { "@types/node": "*", @@ -1277,16 +1277,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", - "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/type-utils": "7.2.0", - "@typescript-eslint/utils": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1295,7 +1295,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1312,19 +1312,19 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", - "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1340,16 +1340,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", - "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1357,18 +1357,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", - "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1384,12 +1384,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", - "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1397,13 +1397,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", - "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1412,7 +1412,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1449,21 +1449,21 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1474,16 +1474,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1497,9 +1497,9 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", - "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz", + "integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -1507,12 +1507,13 @@ "debug": "^4.3.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^4.0.1", + "istanbul-lib-source-maps": "^5.0.4", "istanbul-reports": "^3.1.6", "magic-string": "^0.30.5", "magicast": "^0.3.3", "picocolors": "^1.0.0", "std-env": "^3.5.0", + "strip-literal": "^2.0.0", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.2.0" }, @@ -1520,17 +1521,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.3.1" + "vitest": "1.4.0" } }, "node_modules/@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", + "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", "dev": true, "dependencies": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "chai": "^4.3.10" }, "funding": { @@ -1538,12 +1539,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", + "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", "dev": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.4.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -1552,9 +1553,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", + "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -1566,9 +1567,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", + "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -1578,9 +1579,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2186,9 +2187,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -2198,29 +2199,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { @@ -3109,14 +3110,14 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", "dev": true, "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" @@ -3920,9 +3921,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.37", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.37.tgz", + "integrity": "sha512-7iB/v/r7Woof0glKLH8b1SPHrsX7uhdO+Geb41QpF/+mWZHU3uxxSlN+UXGVit1PawOYDToO+AbZzhBzWRDwbQ==", "dev": true, "funding": [ { @@ -3941,7 +3942,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4296,9 +4297,9 @@ } }, "node_modules/rollup": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", - "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -4311,19 +4312,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.12.0", - "@rollup/rollup-android-arm64": "4.12.0", - "@rollup/rollup-darwin-arm64": "4.12.0", - "@rollup/rollup-darwin-x64": "4.12.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", - "@rollup/rollup-linux-arm64-gnu": "4.12.0", - "@rollup/rollup-linux-arm64-musl": "4.12.0", - "@rollup/rollup-linux-riscv64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-musl": "4.12.0", - "@rollup/rollup-win32-arm64-msvc": "4.12.0", - "@rollup/rollup-win32-ia32-msvc": "4.12.0", - "@rollup/rollup-win32-x64-msvc": "4.12.0", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, @@ -4449,9 +4450,9 @@ } }, "node_modules/socket.io-client": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", - "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -4476,19 +4477,10 @@ "node": ">=10.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4798,9 +4790,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -4886,14 +4878,14 @@ } }, "node_modules/vite": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", - "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz", + "integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.36", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -4941,9 +4933,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", + "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -4977,16 +4969,16 @@ } }, "node_modules/vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", + "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", "dev": true, "dependencies": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/expect": "1.4.0", + "@vitest/runner": "1.4.0", + "@vitest/snapshot": "1.4.0", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -5000,7 +4992,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.3.1", + "vite-node": "1.4.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -5015,8 +5007,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.1", - "@vitest/ui": "1.3.1", + "@vitest/browser": "1.4.0", + "@vitest/ui": "1.4.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/e2e/package.json b/e2e/package.json index 99d3a91cd..0201dde6f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.98.2", + "version": "1.100.0", "description": "", "main": "index.js", "type": "module", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index a13bb58eb..ddc8dd3ef 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -5,7 +5,6 @@ import { LibraryResponseDto, LoginResponseDto, SharedLinkType, - TimeBucketSize, getAllLibraries, getAssetInfo, updateAssets, @@ -942,146 +941,6 @@ describe('/asset', () => { }); }); - describe('GET /asset/time-buckets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/asset/time-buckets').query({ size: TimeBucketSize.Month }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should get time buckets by month', async () => { - const { status, body } = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month }); - - expect(status).toBe(200); - expect(body).toEqual( - expect.arrayContaining([ - { count: 3, timeBucket: '1970-02-01T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, - ]), - ); - }); - - it('should not allow access for unrelated shared links', async () => { - const sharedLink = await utils.createSharedLink(user1.accessToken, { - type: SharedLinkType.Individual, - assetIds: user1Assets.map(({ id }) => id), - }); - - const { status, body } = await request(app) - .get('/asset/time-buckets') - .query({ key: sharedLink.key, size: TimeBucketSize.Month }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.noPermission); - }); - - it('should get time buckets by day', async () => { - const { status, body } = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Day }); - - expect(status).toBe(200); - expect(body).toEqual([ - { count: 2, timeBucket: '1970-02-11T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-02-10T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, - ]); - }); - }); - - describe('GET /asset/time-bucket', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/asset/time-bucket').query({ - size: TimeBucketSize.Month, - timeBucket: '1900-01-01T00:00:00.000Z', - }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should handle 5 digit years', async () => { - const { status, body } = await request(app) - .get('/asset/time-bucket') - .query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' }) - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual([]); - }); - - // TODO enable date string validation while still accepting 5 digit years - // it('should fail if time bucket is invalid', async () => { - // const { status, body } = await request(app) - // .get('/asset/time-bucket') - // .set('Authorization', `Bearer ${user1.accessToken}`) - // .query({ size: TimeBucketSize.Month, timeBucket: 'foo' }); - - // expect(status).toBe(400); - // expect(body).toEqual(errorDto.badRequest); - // }); - - it('should return time bucket', async () => { - const { status, body } = await request(app) - .get('/asset/time-bucket') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' }); - - expect(status).toBe(200); - expect(body).toEqual([]); - }); - - it('should return error if time bucket is requested with partners asset and archived', async () => { - const req1 = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true }); - - expect(req1.status).toBe(400); - expect(req1.body).toEqual(errorDto.badRequest()); - - const req2 = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined }); - - expect(req2.status).toBe(400); - expect(req2.body).toEqual(errorDto.badRequest()); - }); - - it('should return error if time bucket is requested with partners asset and favorite', async () => { - const req1 = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true }); - - expect(req1.status).toBe(400); - expect(req1.body).toEqual(errorDto.badRequest()); - - const req2 = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false }); - - expect(req2.status).toBe(400); - expect(req2.body).toEqual(errorDto.badRequest()); - }); - - it('should return error if time bucket is requested with partners asset and trash', async () => { - const req = await request(app) - .get('/asset/time-buckets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true }); - - expect(req.status).toBe(400); - expect(req.body).toEqual(errorDto.badRequest()); - }); - }); - describe('GET /asset', () => { it('should return stack data', async () => { const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`); diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts index 2b551fd24..ec8c3799c 100644 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ b/e2e/src/api/specs/audit.e2e-spec.ts @@ -12,7 +12,8 @@ describe('/audit', () => { admin = await utils.adminSetup(); }); - describe('GET :/file-report', () => { + // TODO: Enable these tests again once #7436 is resolved as these were flaky + describe.skip('GET :/file-report', () => { it('excludes assets without issues from report', async () => { const [trashedAsset, archivedAsset] = await Promise.all([ utils.createAsset(admin.accessToken), diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/api/specs/memory.e2e-spec.ts new file mode 100644 index 000000000..35af1fea9 --- /dev/null +++ b/e2e/src/api/specs/memory.e2e-spec.ts @@ -0,0 +1,376 @@ +import { + AssetFileUploadResponseDto, + LoginResponseDto, + MemoryResponseDto, + MemoryType, + createMemory, + getMemory, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/memories', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + let adminAsset: AssetFileUploadResponseDto; + let userAsset1: AssetFileUploadResponseDto; + let userAsset2: AssetFileUploadResponseDto; + let userMemory: MemoryResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + [adminAsset, userAsset1, userAsset2] = await Promise.all([ + utils.createAsset(admin.accessToken), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + userMemory = await createMemory( + { + memoryCreateDto: { + type: MemoryType.OnThisDay, + memoryAt: new Date(2021).toISOString(), + data: { year: 2021 }, + assetIds: [], + }, + }, + { headers: asBearerAuth(user.accessToken) }, + ); + }); + + describe('GET /memories', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/memories'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + describe('POST /memories', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/memories'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should validate data when type is on this day', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: {}, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), + ); + }); + + it('should create a new memory', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(201); + expect(body).toEqual({ + id: expect.any(String), + type: 'on_this_day', + data: { year: 2021 }, + createdAt: expect.any(String), + updatedAt: expect.any(String), + deletedAt: null, + seenAt: null, + isSaved: false, + memoryAt: expect.any(String), + ownerId: user.userId, + assets: [], + }); + }); + + it('should create a new memory (with assets)', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + assetIds: [userAsset1.id, userAsset2.id], + }); + + expect(status).toBe(201); + expect(body).toMatchObject({ + id: expect.any(String), + assets: expect.arrayContaining([ + expect.objectContaining({ id: userAsset1.id }), + expect.objectContaining({ id: userAsset2.id }), + ]), + }); + expect(body.assets).toHaveLength(2); + }); + + it('should create a new memory and ignore assets the user does not have access to', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + assetIds: [userAsset1.id, adminAsset.id], + }); + + expect(status).toBe(201); + expect(body).toMatchObject({ + id: expect.any(String), + assets: [expect.objectContaining({ id: userAsset1.id })], + }); + expect(body.assets).toHaveLength(1); + }); + }); + + describe('GET /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .get(`/memories/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .get(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should get the memory', async () => { + const { status, body } = await request(app) + .get(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ id: userMemory.id }); + }); + }); + + describe('PUT /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .put(`/memories/${uuidDto.invalid}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should update the memory', async () => { + const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) }); + expect(before.isSaved).toBe(false); + + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ + id: userMemory.id, + isSaved: true, + }); + }); + }); + + describe('PUT /memories/:id/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .put(`/memories/${uuidDto.invalid}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [uuidDto.invalid] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + + it('should require asset access', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [adminAsset.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ + id: adminAsset.id, + success: false, + error: 'no_permission', + }); + }); + + it('should add assets to the memory', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ id: userAsset1.id, success: true }); + }); + }); + + describe('DELETE /memories/:id/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${uuidDto.invalid}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [uuidDto.invalid] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + + it('should only remove assets in the memory', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [adminAsset.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ + id: adminAsset.id, + success: false, + error: 'not_found', + }); + }); + + it('should remove assets from the memory', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ id: userAsset1.id, success: true }); + }); + }); + + describe('DELETE /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should delete the memory', async () => { + const { status } = await request(app) + .delete(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + }); +}); diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 9c554abc5..afe131228 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -1,4 +1,4 @@ -import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets } from '@immich/sdk'; +import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk'; import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; @@ -7,7 +7,6 @@ import { errorDto } from 'src/responses'; import { app, asBearerAuth, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - const today = DateTime.now(); describe('/search', () => { @@ -19,7 +18,7 @@ describe('/search', () => { let assetCyclamen: AssetFileUploadResponseDto; let assetNotocactus: AssetFileUploadResponseDto; let assetSilver: AssetFileUploadResponseDto; - // let assetDensity: AssetFileUploadResponseDto; + let assetDensity: AssetFileUploadResponseDto; // let assetPhiladelphia: AssetFileUploadResponseDto; // let assetOrychophragmus: AssetFileUploadResponseDto; // let assetRidge: AssetFileUploadResponseDto; @@ -79,6 +78,37 @@ describe('/search', () => { await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); } + // note: the coordinates here are not the actual coordinates of the images and are random for most of them + const cities = [ + { latitude: 48.853_41, longitude: 2.3488 }, // paris + { latitude: 63.0695, longitude: -151.0074 }, // denali + { latitude: 52.524_37, longitude: 13.410_53 }, // berlin + { latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore + { latitude: 41.013_84, longitude: 28.949_66 }, // istanbul + { latitude: 5.556_02, longitude: -0.1969 }, // accra + { latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia + { latitude: 23.133_02, longitude: -82.383_04 }, // havana + { latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi + { latitude: 31.222_22, longitude: 121.458_06 }, // shanghai + { latitude: 47.040_57, longitude: 9.068_04 }, // glarus + { latitude: 38.9711, longitude: -109.7137 }, // thompson springs + { latitude: 40.714_27, longitude: -74.005_97 }, // new york + { latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia + { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh + { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge + { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg + { latitude: 35.6895, longitude: 139.691_71 }, // tokyo + ]; + + const updates = assets.map((asset, i) => + updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }), + ); + + await Promise.all(updates); + for (const asset of assets) { + await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); + } + [ assetFalcon, assetDenali, @@ -92,7 +122,7 @@ describe('/search', () => { assetOneJpg5, assetGlarus, assetSprings, - // assetDensity, + assetDensity, // assetPhiladelphia, // assetOrychophragmus, // assetRidge, @@ -106,7 +136,7 @@ describe('/search', () => { }); afterAll(async () => { - await utils.disconnectWebsocket(websocket); + utils.disconnectWebsocket(websocket); }); describe('POST /search/metadata', () => { @@ -298,15 +328,15 @@ describe('/search', () => { }, { should: 'should search by city', - deferred: () => ({ dto: { city: 'Ralston' }, assets: [assetHeic] }), + deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }), }, { should: 'should search by state', - deferred: () => ({ dto: { state: 'Douglas County, Nebraska' }, assets: [assetHeic] }), + deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }), }, { should: 'should search by country', - deferred: () => ({ dto: { country: 'United States of America' }, assets: [assetHeic] }), + deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }), }, { should: 'should search by make', @@ -370,13 +400,44 @@ describe('/search', () => { expect(body).toEqual(errorDto.unauthorized); }); - it('should get places', async () => { + it('should get relevant places', async () => { + const name = 'Paris'; + const { status, body } = await request(app) - .get('/search/places?name=Paris') + .get(`/search/places?name=${name}`) .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeGreaterThan(10); + if (Array.isArray(body)) { + expect(body.length).toBeGreaterThan(10); + expect(body[0].name).toEqual(name); + expect(body[0].admin2name).toEqual(name); + } + }); + }); + + describe('GET /search/cities', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/search/cities'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get all cities', async () => { + const { status, body } = await request(app) + .get('/search/cities') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(Array.isArray(body)).toBe(true); + if (Array.isArray(body)) { + expect(body.length).toBeGreaterThan(10); + const assetsWithCity = body.filter((asset) => !!asset.exifInfo?.city); + expect(assetsWithCity.length).toEqual(body.length); + const cities = new Set(assetsWithCity.map((asset) => asset.exifInfo.city)); + expect(cities.size).toEqual(body.length); + } }); }); @@ -391,7 +452,21 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=country') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['United States of America']); + expect(body).toEqual([ + 'Cuba', + 'France', + 'Georgia', + 'Germany', + 'Ghana', + 'Japan', + 'Morocco', + "People's Republic of China", + 'Russian Federation', + 'Singapore', + 'Spain', + 'Switzerland', + 'United States of America', + ]); expect(status).toBe(200); }); @@ -399,7 +474,23 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=state') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']); + expect(body).toEqual([ + 'Accra, Greater Accra', + 'Berlin', + 'Glarus, Glarus', + 'Havana', + 'Marrakech, Marrakesh-Safi', + 'Mesa County, Colorado', + 'Neshoba County, Mississippi', + 'New York', + 'Page County, Virginia', + 'Paris, Île-de-France', + 'Province of Córdoba, Andalusia', + 'Shanghai Municipality, Shanghai', + 'St.-Petersburg', + 'Tbilisi', + 'Tokyo', + ]); expect(status).toBe(200); }); @@ -407,7 +498,24 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=city') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['Palisade', 'Ralston']); + expect(body).toEqual([ + 'Accra', + 'Berlin', + 'Glarus', + 'Havana', + 'Marrakesh', + 'Montalbán de Córdoba', + 'New York City', + 'Palisade', + 'Paris', + 'Philadelphia', + 'Saint Petersburg', + 'Shanghai', + 'Singapore', + 'Stanley', + 'Tbilisi', + 'Tokyo', + ]); expect(status).toBe(200); }); diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index c223df487..04cfec91e 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -1,18 +1,23 @@ -import { LoginResponseDto } from '@immich/sdk'; +import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { app, utils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; +const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }); + describe('/system-config', () => { let admin: LoginResponseDto; let nonAdmin: LoginResponseDto; + let asset: AssetFileUploadResponseDto; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup(); nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + + asset = await utils.createAsset(admin.accessToken); }); describe('GET /system-config/map/style.json', () => { @@ -22,6 +27,19 @@ describe('/system-config', () => { expect(body).toEqual(errorDto.unauthorized); }); + it('should allow shared link access', async () => { + const sharedLink = await utils.createSharedLink(admin.accessToken, { + type: SharedLinkType.Individual, + assetIds: [asset.id], + }); + const { status, body } = await request(app) + .get(`/system-config/map/style.json?key=${sharedLink.key}`) + .query({ theme: 'dark' }); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); + }); + it('should throw an error if a theme is not light or dark', async () => { for (const theme of ['dark1', true, 123, '', null, undefined]) { const { status, body } = await request(app) @@ -60,4 +78,25 @@ describe('/system-config', () => { expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); }); }); + + describe('PUT /system-config', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put('/system-config'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should reject an invalid config entry', async () => { + const { status, body } = await request(app) + .put('/system-config') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + ...(await getSystemConfig(admin.accessToken)), + storageTemplate: { enabled: true, hashVerificationEnabled: true, template: '{{foo}}' }, + }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.stringContaining('Invalid storage template'))); + }); + }); }); diff --git a/e2e/src/api/specs/timeline.e2e-spec.ts b/e2e/src/api/specs/timeline.e2e-spec.ts new file mode 100644 index 000000000..84daa19f4 --- /dev/null +++ b/e2e/src/api/specs/timeline.e2e-spec.ts @@ -0,0 +1,193 @@ +import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { createUserDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +// TODO this should probably be a test util function +const today = DateTime.fromObject({ + year: 2023, + month: 11, + day: 3, +}) as DateTime; +const yesterday = today.minus({ days: 1 }); + +describe('/timeline', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + let timeBucketUser: LoginResponseDto; + + let userAssets: AssetFileUploadResponseDto[]; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + [user, timeBucketUser] = await Promise.all([ + utils.userSetup(admin.accessToken, createUserDto.create('1')), + utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')), + ]); + + userAssets = await Promise.all([ + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken, { + isFavorite: true, + isReadOnly: true, + fileCreatedAt: yesterday.toISO(), + fileModifiedAt: yesterday.toISO(), + assetData: { filename: 'example.mp4' }, + }), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + + await Promise.all([ + utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }), + utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }), + utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }), + utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }), + ]); + }); + + describe('GET /timeline/buckets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get time buckets by month', async () => { + const { status, body } = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Month }); + + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + { count: 3, timeBucket: '1970-02-01T00:00:00.000Z' }, + { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, + ]), + ); + }); + + it('should not allow access for unrelated shared links', async () => { + const sharedLink = await utils.createSharedLink(user.accessToken, { + type: SharedLinkType.Individual, + assetIds: userAssets.map(({ id }) => id), + }); + + const { status, body } = await request(app) + .get('/timeline/buckets') + .query({ key: sharedLink.key, size: TimeBucketSize.Month }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should get time buckets by day', async () => { + const { status, body } = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Day }); + + expect(status).toBe(200); + expect(body).toEqual([ + { count: 2, timeBucket: '1970-02-11T00:00:00.000Z' }, + { count: 1, timeBucket: '1970-02-10T00:00:00.000Z' }, + { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, + ]); + }); + + it('should return error if time bucket is requested with partners asset and archived', async () => { + const req1 = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorDto.badRequest()); + + const req2 = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${user.accessToken}`) + .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorDto.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and favorite', async () => { + const req1 = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorDto.badRequest()); + + const req2 = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorDto.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and trash', async () => { + const req = await request(app) + .get('/timeline/buckets') + .set('Authorization', `Bearer ${user.accessToken}`) + .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true }); + + expect(req.status).toBe(400); + expect(req.body).toEqual(errorDto.badRequest()); + }); + }); + + describe('GET /timeline/bucket', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/timeline/bucket').query({ + size: TimeBucketSize.Month, + timeBucket: '1900-01-01T00:00:00.000Z', + }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should handle 5 digit years', async () => { + const { status, body } = await request(app) + .get('/timeline/bucket') + .query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' }) + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual([]); + }); + + // TODO enable date string validation while still accepting 5 digit years + // it('should fail if time bucket is invalid', async () => { + // const { status, body } = await request(app) + // .get('/timeline/bucket') + // .set('Authorization', `Bearer ${user.accessToken}`) + // .query({ size: TimeBucketSize.Month, timeBucket: 'foo' }); + + // expect(status).toBe(400); + // expect(body).toEqual(errorDto.badRequest); + // }); + + it('should return time bucket', async () => { + const { status, body } = await request(app) + .get('/timeline/bucket') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' }); + + expect(status).toBe(200); + expect(body).toEqual([]); + }); + }); +}); diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index 42877221f..0fb48188a 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -2,25 +2,25 @@ import { stat } from 'node:fs/promises'; import { app, immichCli, utils } from 'src/utils'; import { beforeEach, describe, expect, it } from 'vitest'; -describe(`immich login-key`, () => { +describe(`immich login`, () => { beforeEach(async () => { await utils.resetDatabase(); }); it('should require a url', async () => { - const { stderr, exitCode } = await immichCli(['login-key']); + const { stderr, exitCode } = await immichCli(['login']); expect(stderr).toBe("error: missing required argument 'url'"); expect(exitCode).toBe(1); }); it('should require a key', async () => { - const { stderr, exitCode } = await immichCli(['login-key', app]); + const { stderr, exitCode } = await immichCli(['login', app]); expect(stderr).toBe("error: missing required argument 'key'"); expect(exitCode).toBe(1); }); it('should require a valid key', async () => { - const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']); + const { stderr, exitCode } = await immichCli(['login', app, 'immich-is-so-cool']); expect(stderr).toContain('Failed to connect to server'); expect(stderr).toContain('Invalid API key'); expect(stderr).toContain('401'); @@ -30,7 +30,7 @@ describe(`immich login-key`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); const key = await utils.createApiKey(admin.accessToken); - const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]); + const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283/api', 'Logged in as admin@immich.cloud', @@ -47,7 +47,7 @@ describe(`immich login-key`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); const key = await utils.createApiKey(admin.accessToken); - const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]); + const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283', 'Discovered API at http://127.0.0.1:2283/api', diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index 6efe002b8..13eefd3df 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -4,19 +4,23 @@ import { beforeAll, describe, expect, it } from 'vitest'; describe(`immich server-info`, () => { beforeAll(async () => { await utils.resetDatabase(); - await utils.cliLogin(); + const admin = await utils.adminSetup(); + await utils.cliLogin(admin.accessToken); }); it('should return the server info', async () => { const { stderr, stdout, exitCode } = await immichCli(['server-info']); expect(stdout.split('\n')).toEqual([ - expect.stringContaining('Server Version:'), - expect.stringContaining('Image Types:'), - expect.stringContaining('Video Types:'), - 'Statistics:', - ' Images: 0', - ' Videos: 0', - ' Total: 0', + expect.stringContaining('Server Info (via admin@immich.cloud'), + ' Url: http://127.0.0.1:2283/api', + expect.stringContaining('Version:'), + ' Formats:', + expect.stringContaining('Images:'), + expect.stringContaining('Videos:'), + ' Statistics:', + ' Images: 0', + ' Videos: 0', + ' Total: 0', ]); expect(stderr).toBe(''); expect(exitCode).toBe(0); diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index bc4382f98..a74a57c71 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -1,20 +1,69 @@ -import { getAllAlbums, getAllAssets } from '@immich/sdk'; +import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; describe(`immich upload`, () => { + let admin: LoginResponseDto; let key: string; beforeAll(async () => { await utils.resetDatabase(); - key = await utils.cliLogin(); + + admin = await utils.adminSetup(); + key = await utils.cliLogin(admin.accessToken); }); beforeEach(async () => { await utils.resetDatabase(['assets', 'albums']); }); + describe(`immich upload /path/to/file.jpg`, () => { + it('should upload a single file', async () => { + const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); + expect(stderr).toBe(''); + expect(stdout.split('\n')).toEqual( + expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]), + ); + expect(exitCode).toBe(0); + + const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); + expect(assets.length).toBe(1); + }); + + it('should skip a duplicate file', async () => { + const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); + expect(first.stderr).toBe(''); + expect(first.stdout.split('\n')).toEqual( + expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]), + ); + expect(first.exitCode).toBe(0); + + const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); + expect(assets.length).toBe(1); + + const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); + expect(second.stderr).toBe(''); + expect(second.stdout.split('\n')).toEqual( + expect.arrayContaining([ + expect.stringContaining('Found 0 new files and 1 duplicate'), + expect.stringContaining('All assets were already uploaded, nothing to do'), + ]), + ); + expect(first.exitCode).toBe(0); + }); + + it('should skip files that do not exist', async () => { + const { stderr, stdout, exitCode } = await immichCli(['upload', `/path/to/file`]); + expect(stderr).toBe(''); + expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')])); + expect(exitCode).toBe(0); + + const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); + expect(assets.length).toBe(0); + }); + }); + describe('immich upload --recursive', () => { it('should upload a folder recursively', async () => { const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 8ca7fba60..d8302a9e3 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -39,7 +39,7 @@ import { makeRandomImage } from 'src/generators'; import request from 'supertest'; type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; -type EventType = 'assetUpload' | 'assetDelete' | 'userDelete'; +type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete'; type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number }; type AdminSetupOptions = { onboarding?: boolean }; type AssetData = { bytes?: Buffer; filename: string }; @@ -82,6 +82,7 @@ let client: pg.Client | null = null; const events: Record> = { assetUpload: new Set(), + assetUpdate: new Set(), assetDelete: new Set(), userDelete: new Set(), }; @@ -185,6 +186,7 @@ export const utils = { websocket .on('connect', () => resolve(websocket)) .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id })) + .on('on_asset_update', (data: AssetResponseDto) => onEvent({ event: 'assetUpdate', id: data.id })) .on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId })) .on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId })) .connect(); @@ -404,10 +406,9 @@ export const utils = { }, ]), - cliLogin: async () => { - const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); - await immichCli(['login-key', app, `${key.secret}`]); + cliLogin: async (accessToken: string) => { + const key = await utils.createApiKey(accessToken); + await immichCli(['login', app, `${key.secret}`]); return key.secret; }, }; diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 2df540f09..3f111401c 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:991e20a11120277e977cadbc104e7a9b196a68a346597879821b19034285a403 as builder-cpu +FROM python:3.11-bookworm@sha256:e2ed446c899827ed992f8a5a8875fa0853fcab32581e61418b650322061aa3c4 as builder-cpu FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino USER root @@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:a2eb07f336e4f194358382611b4fea136c632b40baa6314cb27a366deeaf0144 as prod-cpu +FROM python:3.11-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as prod-cpu FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino USER root diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 1016b330c..fa3f7068e 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:3624db3aee11d2f3f00d25f691aaaf8834b8bc4ec1b340dcdb48ef37281ea604 as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 7a0c66bdc..6b7ebe254 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -877,13 +877,13 @@ tqdm = ["tqdm"] [[package]] name = "ftfy" -version = "6.1.3" +version = "6.2.0" description = "Fixes mojibake and other problems with Unicode, after the fact" optional = false python-versions = ">=3.8,<4" files = [ - {file = "ftfy-6.1.3-py3-none-any.whl", hash = "sha256:e49c306c06a97f4986faa7a8740cfe3c13f3106e85bcec73eb629817e671557c"}, - {file = "ftfy-6.1.3.tar.gz", hash = "sha256:693274aead811cff24c1e8784165aa755cd2f6e442a5ec535c7d697f6422a422"}, + {file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"}, + {file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"}, ] [package.dependencies] @@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "huggingface-hub" -version = "0.21.4" +version = "0.22.2" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.21.4-py3-none-any.whl", hash = "sha256:df37c2c37fc6c82163cdd8a67ede261687d80d1e262526d6c0ce73b6b3630a7b"}, - {file = "huggingface_hub-0.21.4.tar.gz", hash = "sha256:e1f4968c93726565a80edf6dc309763c7b546d0cfe79aa221206034d50155531"}, + {file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"}, + {file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"}, ] [package.dependencies] @@ -1293,15 +1293,16 @@ tqdm = ">=4.42.1" typing-extensions = ">=3.7.4.3" [package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf-transfer (>=0.1.4)"] -inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] -quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] +inference = ["aiohttp", "minijinja (>=1.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"] tensorflow = ["graphviz", "pydot", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] torch = ["safetensors", "torch"] typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] @@ -1567,13 +1568,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.24.0" +version = "2.24.1" description = "Developer friendly load testing framework" optional = false python-versions = ">=3.8" files = [ - {file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"}, - {file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"}, + {file = "locust-2.24.1-py3-none-any.whl", hash = "sha256:7f6ed4dc289aad66c304582e6d25e4de5d7c3b175b580332442ab2be35b9d916"}, + {file = "locust-2.24.1.tar.gz", hash = "sha256:094161d44d94839bf1120fd7898b7abb9c143833743ba7c096beb470a236b9a7"}, ] [package.dependencies] @@ -2495,13 +2496,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm [[package]] name = "pytest-asyncio" -version = "0.23.5.post1" +version = "0.23.6" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, - {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, ] [package.dependencies] @@ -2531,17 +2532,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] @@ -2844,28 +2845,28 @@ files = [ [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, + {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, + {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, + {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, + {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, ] [[package]] @@ -3289,13 +3290,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.28.0" +version = "0.29.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, - {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, ] [package.dependencies] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index c0e549af5..dafd9d097 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.98.2" +version = "1.100.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/.fvm/fvm_config.json b/mobile/.fvm/fvm_config.json index 3e7195748..36eb0ad1c 100644 --- a/mobile/.fvm/fvm_config.json +++ b/mobile/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.16.9", + "flutterSdkVersion": "3.19.3", "flavors": {} } \ No newline at end of file diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 5b91b2258..000000000 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 18470610c..000000000 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index f0d6d66ac..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 6eb60e8b7..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index c8842b651..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml index 6452c29ba..d14fce37e 100644 --- a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,70 +1,27 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml index d98b89c74..8b19a433f 100644 --- a/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,70 +1,27 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 5f349f7f4..f606c4d83 100644 --- a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index a11d60267..f7edc199d 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 128, - "android.injected.version.name" => "1.98.2", + "android.injected.version.code" => 130, + "android.injected.version.name" => "1.100.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 0dcf80996..c9be4ad53 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 9da867386..e350e5b62 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -190,6 +190,7 @@ "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", "exif_bottom_sheet_people": "PEOPLE", + "exif_bottom_sheet_person_add_person": "Add name", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", @@ -282,6 +283,10 @@ "map_settings_only_show_favorites": "Show Favorite Only", "map_settings_theme_settings": "Map Theme", "map_zoom_to_see_photos": "Zoom out to see photos", + "memories_all_caught_up": "All caught up", + "memories_check_back_tomorrow": "Check back tomorrow for more memories", + "memories_start_over": "Start Over", + "memories_swipe_to_close": "Swipe up to close", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", @@ -481,4 +486,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} +} \ No newline at end of file diff --git a/mobile/assets/immich-logo-inline-dark.svg b/mobile/assets/immich-logo-inline-dark.svg new file mode 100644 index 000000000..8d72e075b --- /dev/null +++ b/mobile/assets/immich-logo-inline-dark.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/assets/immich-logo-inline-light.svg b/mobile/assets/immich-logo-inline-light.svg new file mode 100644 index 000000000..d40a27a2b --- /dev/null +++ b/mobile/assets/immich-logo-inline-light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 6081988b7..5493fc284 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -155,7 +155,7 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 @@ -180,4 +180,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 5f44646ec..48c76d1e0 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -172,7 +172,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 43b89d762..1860f98f3 100644 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.98.2 + 1.100.0 CFBundleSignature ???? CFBundleVersion - 144 + 146 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 00cbeb8ac..9b9766b8c 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.98.2" + version_number: "1.100.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index f1e83434b..47696ac0c 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index f66a30f31..a72620b86 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -243,12 +243,7 @@ class AlbumService { } } - await _db.writeTxn(() async { - await album.assets.update(link: successAssets); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); + await _updateAssets(album.id, add: successAssets); return AddAssetsResponse( alreadyInAlbum: duplicatedAssets, @@ -257,11 +252,28 @@ class AlbumService { } } catch (e) { debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}"); - return null; } return null; } + Future _updateAssets( + int albumId, { + Iterable add = const [], + Iterable remove = const [], + }) { + return _db.writeTxn(() async { + final album = await _db.albums.get(albumId); + if (album == null) return; + await album.assets.update(link: add, unlink: remove); + album.startDate = + await album.assets.filter().fileCreatedAtProperty().min(); + album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); + album.lastModifiedAssetTimestamp = + await album.assets.filter().updatedAtProperty().max(); + await _db.albums.put(album); + }); + } + Future addAdditionalUserToAlbum( List sharedUserIds, Album album, @@ -342,7 +354,7 @@ class AlbumService { await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me"); return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + debugPrint("Error leaveAlbum ${e.toString()}"); return false; } } @@ -352,24 +364,25 @@ class AlbumService { Iterable assets, ) async { try { - await _apiService.albumApi.removeAssetFromAlbum( + final response = await _apiService.albumApi.removeAssetFromAlbum( album.remoteId!, BulkIdsDto( ids: assets.map((asset) => asset.remoteId!).toList(), ), ); - await _db.writeTxn(() async { - await album.assets.update(unlink: assets); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); - - return true; + if (response != null) { + final toRemove = response.every((e) => e.success) + ? assets + : response + .where((e) => e.success) + .map((e) => assets.firstWhere((a) => a.remoteId == e.id)); + await _updateAssets(album.id, remove: toRemove); + return true; + } } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); - return false; + debugPrint("Error removeAssetFromAlbum ${e.toString()}"); } + return false; } Future removeUserFromAlbum( @@ -413,7 +426,7 @@ class AlbumService { return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + debugPrint("Error changeTitleAlbum ${e.toString()}"); return false; } } diff --git a/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart b/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart index e5204acde..209955bcb 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart @@ -16,8 +16,6 @@ class ImageLoader { required ImageCacheManager cache, required ImageDecoderCallback decode, StreamController? chunkEvents, - int? height, - int? width, }) async { final headers = { 'x-immich-user-token': Store.get(StoreKey.accessToken), @@ -25,10 +23,8 @@ class ImageLoader { final stream = cache.getImageFile( uri, - withProgress: true, + withProgress: chunkEvents != null, headers: headers, - maxHeight: height, - maxWidth: width, ); await for (final result in stream) { @@ -40,13 +36,9 @@ class ImageLoader { expectedTotalBytes: result.totalSize, ), ); - } - - if (result is FileInfo) { + } else if (result is FileInfo) { // We have the file - final file = result.file; - final bytes = await file.readAsBytes(); - final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path); final decoded = await decode(buffer); return decoded; } diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart index 620f4f3cd..b0dd65371 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart @@ -26,7 +26,7 @@ class ExifPeople extends ConsumerWidget { .watch(assetPeopleNotifierProvider(asset)) .value ?.where((p) => !p.isHidden); - final double imageSize = math.min(context.width / 3, 120); + final double imageSize = math.min(context.width / 3, 150); showPersonNameEditModel( String personId, diff --git a/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart b/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart index cf1de0383..d66220f1a 100644 --- a/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart +++ b/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart @@ -80,7 +80,6 @@ class _AssetDragRegionState extends State { recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); recognizer.onLongPressStart = (details) => _onLongPressStart(details); recognizer.onLongPressUp = _onLongPressEnd; - recognizer.onLongPressCancel = _onLongPressEnd; } AssetIndex? _getValueKeyAtPositon(Offset position) { diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 687e7aaac..f075280ae 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -42,7 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget { this.assetsPerRow, this.showStorageIndicator, this.listener, - this.margin = 5.0, + this.margin = 2.0, this.selectionActive = false, this.preselectedAssets, this.canDeselect = true, diff --git a/mobile/lib/modules/memories/ui/memory_epilogue.dart b/mobile/lib/modules/memories/ui/memory_epilogue.dart index 8dd28637d..b817d67f0 100644 --- a/mobile/lib/modules/memories/ui/memory_epilogue.dart +++ b/mobile/lib/modules/memories/ui/memory_epilogue.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -55,27 +56,27 @@ class _MemoryEpilogueState extends State ), const SizedBox(height: 16.0), Text( - 'All caught up', + "memories_all_caught_up", style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), const SizedBox(height: 16.0), Text( - 'Check back tomorrow for more memories', + "memories_check_back_tomorrow", style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), const SizedBox(height: 16.0), TextButton( onPressed: widget.onStartOver, child: Text( - 'Start Over', + "memories_start_over", style: context.textTheme.displayMedium?.copyWith( color: immichDarkThemePrimaryColor, ), - ), + ).tr(), ), ], ), @@ -106,11 +107,11 @@ class _MemoryEpilogueState extends State ), ), Text( - 'Swipe up to close', + "memories_swipe_to_close", style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), ], ), ), diff --git a/mobile/lib/modules/search/models/curated_content.dart b/mobile/lib/modules/search/models/curated_content.dart index df7cb032c..87e98bb75 100644 --- a/mobile/lib/modules/search/models/curated_content.dart +++ b/mobile/lib/modules/search/models/curated_content.dart @@ -1,15 +1,60 @@ -/// A wrapper for [CuratedLocationsResponseDto] objects -/// and [CuratedObjectsResponseDto] to be displayed in -/// a view -class CuratedContent { - /// The label to show associated with this curated object - final String label; - - /// The id to lookup the asset from the server - final String id; - - CuratedContent({ - required this.id, - required this.label, - }); -} +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +/// A wrapper for [CuratedLocationsResponseDto] objects +/// and [CuratedObjectsResponseDto] to be displayed in +/// a view +class CuratedContent { + /// The label to show associated with this curated object + final String label; + + /// The id to lookup the asset from the server + final String id; + + CuratedContent({ + required this.label, + required this.id, + }); + + CuratedContent copyWith({ + String? label, + String? id, + }) { + return CuratedContent( + label: label ?? this.label, + id: id ?? this.id, + ); + } + + Map toMap() { + return { + 'label': label, + 'id': id, + }; + } + + factory CuratedContent.fromMap(Map map) { + return CuratedContent( + label: map['label'] as String, + id: map['id'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory CuratedContent.fromJson(String source) => + CuratedContent.fromMap(json.decode(source) as Map); + + @override + String toString() => 'CuratedContent(label: $label, id: $id)'; + + @override + bool operator ==(covariant CuratedContent other) { + if (identical(this, other)) return true; + + return other.label == label && other.id == id; + } + + @override + int get hashCode => label.hashCode ^ id.hashCode; +} diff --git a/mobile/lib/modules/search/models/search_filter.dart b/mobile/lib/modules/search/models/search_filter.dart new file mode 100644 index 000000000..337da9266 --- /dev/null +++ b/mobile/lib/modules/search/models/search_filter.dart @@ -0,0 +1,310 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:openapi/api.dart'; + +class SearchLocationFilter { + String? country; + String? state; + String? city; + SearchLocationFilter({ + this.country, + this.state, + this.city, + }); + + SearchLocationFilter copyWith({ + String? country, + String? state, + String? city, + }) { + return SearchLocationFilter( + country: country ?? this.country, + state: state ?? this.state, + city: city ?? this.city, + ); + } + + Map toMap() { + return { + 'country': country, + 'state': state, + 'city': city, + }; + } + + factory SearchLocationFilter.fromMap(Map map) { + return SearchLocationFilter( + country: map['country'] != null ? map['country'] as String : null, + state: map['state'] != null ? map['state'] as String : null, + city: map['city'] != null ? map['city'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchLocationFilter.fromJson(String source) => + SearchLocationFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchLocationFilter(country: $country, state: $state, city: $city)'; + + @override + bool operator ==(covariant SearchLocationFilter other) { + if (identical(this, other)) return true; + + return other.country == country && + other.state == state && + other.city == city; + } + + @override + int get hashCode => country.hashCode ^ state.hashCode ^ city.hashCode; +} + +class SearchCameraFilter { + String? make; + String? model; + SearchCameraFilter({ + this.make, + this.model, + }); + + SearchCameraFilter copyWith({ + String? make, + String? model, + }) { + return SearchCameraFilter( + make: make ?? this.make, + model: model ?? this.model, + ); + } + + Map toMap() { + return { + 'make': make, + 'model': model, + }; + } + + factory SearchCameraFilter.fromMap(Map map) { + return SearchCameraFilter( + make: map['make'] != null ? map['make'] as String : null, + model: map['model'] != null ? map['model'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchCameraFilter.fromJson(String source) => + SearchCameraFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SearchCameraFilter(make: $make, model: $model)'; + + @override + bool operator ==(covariant SearchCameraFilter other) { + if (identical(this, other)) return true; + + return other.make == make && other.model == model; + } + + @override + int get hashCode => make.hashCode ^ model.hashCode; +} + +class SearchDateFilter { + DateTime? takenBefore; + DateTime? takenAfter; + SearchDateFilter({ + this.takenBefore, + this.takenAfter, + }); + + SearchDateFilter copyWith({ + DateTime? takenBefore, + DateTime? takenAfter, + }) { + return SearchDateFilter( + takenBefore: takenBefore ?? this.takenBefore, + takenAfter: takenAfter ?? this.takenAfter, + ); + } + + Map toMap() { + return { + 'takenBefore': takenBefore?.millisecondsSinceEpoch, + 'takenAfter': takenAfter?.millisecondsSinceEpoch, + }; + } + + factory SearchDateFilter.fromMap(Map map) { + return SearchDateFilter( + takenBefore: map['takenBefore'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int) + : null, + takenAfter: map['takenAfter'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int) + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchDateFilter.fromJson(String source) => + SearchDateFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)'; + + @override + bool operator ==(covariant SearchDateFilter other) { + if (identical(this, other)) return true; + + return other.takenBefore == takenBefore && other.takenAfter == takenAfter; + } + + @override + int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode; +} + +class SearchDisplayFilters { + bool isNotInAlbum = false; + bool isArchive = false; + bool isFavorite = false; + SearchDisplayFilters({ + required this.isNotInAlbum, + required this.isArchive, + required this.isFavorite, + }); + + SearchDisplayFilters copyWith({ + bool? isNotInAlbum, + bool? isArchive, + bool? isFavorite, + }) { + return SearchDisplayFilters( + isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum, + isArchive: isArchive ?? this.isArchive, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + + Map toMap() { + return { + 'isNotInAlbum': isNotInAlbum, + 'isArchive': isArchive, + 'isFavorite': isFavorite, + }; + } + + factory SearchDisplayFilters.fromMap(Map map) { + return SearchDisplayFilters( + isNotInAlbum: map['isNotInAlbum'] as bool, + isArchive: map['isArchive'] as bool, + isFavorite: map['isFavorite'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchDisplayFilters.fromJson(String source) => + SearchDisplayFilters.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)'; + + @override + bool operator ==(covariant SearchDisplayFilters other) { + if (identical(this, other)) return true; + + return other.isNotInAlbum == isNotInAlbum && + other.isArchive == isArchive && + other.isFavorite == isFavorite; + } + + @override + int get hashCode => + isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; +} + +class SearchFilter { + String? context; + String? filename; + Set people; + SearchLocationFilter location; + SearchCameraFilter camera; + SearchDateFilter date; + SearchDisplayFilters display; + + // Enum + AssetType mediaType; + + SearchFilter({ + this.context, + this.filename, + required this.people, + required this.location, + required this.camera, + required this.date, + required this.display, + required this.mediaType, + }); + + SearchFilter copyWith({ + String? context, + String? filename, + Set? people, + SearchLocationFilter? location, + SearchCameraFilter? camera, + SearchDateFilter? date, + SearchDisplayFilters? display, + AssetType? mediaType, + }) { + return SearchFilter( + context: context ?? this.context, + filename: filename ?? this.filename, + people: people ?? this.people, + location: location ?? this.location, + camera: camera ?? this.camera, + date: date ?? this.date, + display: display ?? this.display, + mediaType: mediaType ?? this.mediaType, + ); + } + + @override + String toString() { + return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)'; + } + + @override + bool operator ==(covariant SearchFilter other) { + if (identical(this, other)) return true; + + return other.context == context && + other.filename == filename && + other.people == people && + other.location == location && + other.camera == camera && + other.date == date && + other.display == display && + other.mediaType == mediaType; + } + + @override + int get hashCode { + return context.hashCode ^ + filename.hashCode ^ + people.hashCode ^ + location.hashCode ^ + camera.hashCode ^ + date.hashCode ^ + display.hashCode ^ + mediaType.hashCode; + } +} diff --git a/mobile/lib/modules/search/providers/paginated_search.provider.dart b/mobile/lib/modules/search/providers/paginated_search.provider.dart new file mode 100644 index 000000000..e20e37c52 --- /dev/null +++ b/mobile/lib/modules/search/providers/paginated_search.provider.dart @@ -0,0 +1,62 @@ +import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/services/search.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'paginated_search.provider.g.dart'; + +@riverpod +class PaginatedSearch extends _$PaginatedSearch { + Future?> _search(SearchFilter filter, int page) async { + final service = ref.read(searchServiceProvider); + final result = await service.search(filter, page); + + return result; + } + + @override + Future> build() async { + return []; + } + + Future> getNextPage(SearchFilter filter, int nextPage) async { + state = const AsyncValue.loading(); + + final newState = await AsyncValue.guard(() async { + final assets = await _search(filter, nextPage); + + if (assets != null) { + return [...?state.value, ...assets]; + } + }); + + state = newState.valueOrNull == null + ? const AsyncValue.data([]) + : AsyncValue.data(newState.value!); + + return newState.valueOrNull ?? []; + } + + clear() { + state = const AsyncValue.data([]); + } +} + +@riverpod +AsyncValue paginatedSearchRenderList( + PaginatedSearchRenderListRef ref, +) { + final assets = ref.watch(paginatedSearchProvider).value; + + if (assets != null) { + return ref.watch( + renderListProviderWithGrouping( + (assets, GroupAssetsBy.none), + ), + ); + } else { + return const AsyncValue.loading(); + } +} diff --git a/mobile/lib/modules/search/providers/paginated_search.provider.g.dart b/mobile/lib/modules/search/providers/paginated_search.provider.g.dart new file mode 100644 index 000000000..3357be777 --- /dev/null +++ b/mobile/lib/modules/search/providers/paginated_search.provider.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'paginated_search.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$paginatedSearchRenderListHash() => + r'c2cc2381ee6ea8f8e08d6d4c1289bbf0c6b9647e'; + +/// See also [paginatedSearchRenderList]. +@ProviderFor(paginatedSearchRenderList) +final paginatedSearchRenderListProvider = + AutoDisposeProvider>.internal( + paginatedSearchRenderList, + name: r'paginatedSearchRenderListProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$paginatedSearchRenderListHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PaginatedSearchRenderListRef + = AutoDisposeProviderRef>; +String _$paginatedSearchHash() => r'8312f358261368cf2b5572b839fdd8f8fbe9a62e'; + +/// See also [PaginatedSearch]. +@ProviderFor(PaginatedSearch) +final paginatedSearchProvider = + AutoDisposeAsyncNotifierProvider>.internal( + PaginatedSearch.new, + name: r'paginatedSearchProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$paginatedSearchHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PaginatedSearch = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/providers/people.provider.dart b/mobile/lib/modules/search/providers/people.provider.dart index 6009ee53a..398d1122a 100644 --- a/mobile/lib/modules/search/providers/people.provider.dart +++ b/mobile/lib/modules/search/providers/people.provider.dart @@ -1,51 +1,49 @@ -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; -import 'package:immich_mobile/modules/search/services/person.service.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'people.provider.g.dart'; - -@riverpod -Future> getCuratedPeople( - GetCuratedPeopleRef ref, -) async { - final PersonService personService = ref.read(personServiceProvider); - - final curatedPeople = await personService.getCuratedPeople(); - - return curatedPeople - .map((p) => CuratedContent(id: p.id, label: p.name)) - .toList(); -} - -@riverpod -Future personAssets(PersonAssetsRef ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - if (assets == null) { - return RenderList.empty(); - } - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - -@riverpod -Future updatePersonName( - UpdatePersonNameRef ref, - String personId, - String updatedName, -) async { - final PersonService personService = ref.read(personServiceProvider); - final person = await personService.updateName(personId, updatedName); - - if (person != null && person.name == updatedName) { - ref.invalidate(getCuratedPeopleProvider); - return true; - } - return false; -} +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/services/person.service.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'people.provider.g.dart'; + +@riverpod +Future> getAllPeople( + GetAllPeopleRef ref, +) async { + final PersonService personService = ref.read(personServiceProvider); + + final people = await personService.getAllPeople(); + + return people; +} + +@riverpod +Future personAssets(PersonAssetsRef ref, String personId) async { + final PersonService personService = ref.read(personServiceProvider); + final assets = await personService.getPersonAssets(personId); + if (assets == null) { + return RenderList.empty(); + } + + final settings = ref.read(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return await RenderList.fromAssets(assets, groupBy); +} + +@riverpod +Future updatePersonName( + UpdatePersonNameRef ref, + String personId, + String updatedName, +) async { + final PersonService personService = ref.read(personServiceProvider); + final person = await personService.updateName(personId, updatedName); + + if (person != null && person.name == updatedName) { + ref.invalidate(getAllPeopleProvider); + return true; + } + return false; +} diff --git a/mobile/lib/modules/search/providers/people.provider.g.dart b/mobile/lib/modules/search/providers/people.provider.g.dart index c13c2c160..c68f7a75f 100644 --- a/mobile/lib/modules/search/providers/people.provider.g.dart +++ b/mobile/lib/modules/search/providers/people.provider.g.dart @@ -6,23 +6,21 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0'; +String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd'; -/// See also [getCuratedPeople]. -@ProviderFor(getCuratedPeople) -final getCuratedPeopleProvider = - AutoDisposeFutureProvider>.internal( - getCuratedPeople, - name: r'getCuratedPeopleProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$getCuratedPeopleHash, +/// See also [getAllPeople]. +@ProviderFor(getAllPeople) +final getAllPeopleProvider = + AutoDisposeFutureProvider>.internal( + getAllPeople, + name: r'getAllPeopleProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$getAllPeopleHash, dependencies: null, allTransitiveDependencies: null, ); -typedef GetCuratedPeopleRef - = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; /// Copied from Dart SDK @@ -172,7 +170,7 @@ class _PersonAssetsProviderElement String get personId => (origin as PersonAssetsProvider).personId; } -String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd'; +String _$updatePersonNameHash() => r'7145aaaf6fc38fdafe3a283ebf3d3f4fd0774cd2'; /// See also [updatePersonName]. @ProviderFor(updatePersonName) diff --git a/mobile/lib/modules/search/providers/recently_added.provider.dart b/mobile/lib/modules/search/providers/recently_added.provider.dart index 5b7380979..4e6d2c156 100644 --- a/mobile/lib/modules/search/providers/recently_added.provider.dart +++ b/mobile/lib/modules/search/providers/recently_added.provider.dart @@ -1,13 +1,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:isar/isar.dart'; final recentlyAddedProvider = FutureProvider>((ref) async { + final user = ref.read(currentUserProvider); + if (user == null) return []; + return ref .watch(dbProvider) .assets .where() + .ownerIdEqualToAnyChecksum(user.isarId) .sortByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/modules/search/providers/search_filter.provider.dart b/mobile/lib/modules/search/providers/search_filter.provider.dart new file mode 100644 index 000000000..1a4914b41 --- /dev/null +++ b/mobile/lib/modules/search/providers/search_filter.provider.dart @@ -0,0 +1,27 @@ +import 'package:immich_mobile/modules/search/services/search.service.dart'; +import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'search_filter.provider.g.dart'; + +@riverpod +Future> getSearchSuggestions( + GetSearchSuggestionsRef ref, + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, +}) async { + final SearchService service = ref.read(searchServiceProvider); + + final suggestions = await service.getSearchSuggestions( + type, + country: locationCountry, + state: locationState, + make: make, + model: model, + ); + + return suggestions ?? []; +} diff --git a/mobile/lib/modules/search/providers/search_filter.provider.g.dart b/mobile/lib/modules/search/providers/search_filter.provider.g.dart new file mode 100644 index 000000000..d5cdaa031 --- /dev/null +++ b/mobile/lib/modules/search/providers/search_filter.provider.g.dart @@ -0,0 +1,229 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'search_filter.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getSearchSuggestionsHash() => + r'bc1e9a1a060868f14e6eb970d2251dbfe39c6866'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [getSearchSuggestions]. +@ProviderFor(getSearchSuggestions) +const getSearchSuggestionsProvider = GetSearchSuggestionsFamily(); + +/// See also [getSearchSuggestions]. +class GetSearchSuggestionsFamily extends Family>> { + /// See also [getSearchSuggestions]. + const GetSearchSuggestionsFamily(); + + /// See also [getSearchSuggestions]. + GetSearchSuggestionsProvider call( + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, + }) { + return GetSearchSuggestionsProvider( + type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ); + } + + @override + GetSearchSuggestionsProvider getProviderOverride( + covariant GetSearchSuggestionsProvider provider, + ) { + return call( + provider.type, + locationCountry: provider.locationCountry, + locationState: provider.locationState, + make: provider.make, + model: provider.model, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'getSearchSuggestionsProvider'; +} + +/// See also [getSearchSuggestions]. +class GetSearchSuggestionsProvider + extends AutoDisposeFutureProvider> { + /// See also [getSearchSuggestions]. + GetSearchSuggestionsProvider( + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, + }) : this._internal( + (ref) => getSearchSuggestions( + ref as GetSearchSuggestionsRef, + type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ), + from: getSearchSuggestionsProvider, + name: r'getSearchSuggestionsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getSearchSuggestionsHash, + dependencies: GetSearchSuggestionsFamily._dependencies, + allTransitiveDependencies: + GetSearchSuggestionsFamily._allTransitiveDependencies, + type: type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ); + + GetSearchSuggestionsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.type, + required this.locationCountry, + required this.locationState, + required this.make, + required this.model, + }) : super.internal(); + + final SearchSuggestionType type; + final String? locationCountry; + final String? locationState; + final String? make; + final String? model; + + @override + Override overrideWith( + FutureOr> Function(GetSearchSuggestionsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GetSearchSuggestionsProvider._internal( + (ref) => create(ref as GetSearchSuggestionsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + type: type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _GetSearchSuggestionsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GetSearchSuggestionsProvider && + other.type == type && + other.locationCountry == locationCountry && + other.locationState == locationState && + other.make == make && + other.model == model; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, type.hashCode); + hash = _SystemHash.combine(hash, locationCountry.hashCode); + hash = _SystemHash.combine(hash, locationState.hashCode); + hash = _SystemHash.combine(hash, make.hashCode); + hash = _SystemHash.combine(hash, model.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef> { + /// The parameter `type` of this provider. + SearchSuggestionType get type; + + /// The parameter `locationCountry` of this provider. + String? get locationCountry; + + /// The parameter `locationState` of this provider. + String? get locationState; + + /// The parameter `make` of this provider. + String? get make; + + /// The parameter `model` of this provider. + String? get model; +} + +class _GetSearchSuggestionsProviderElement + extends AutoDisposeFutureProviderElement> + with GetSearchSuggestionsRef { + _GetSearchSuggestionsProviderElement(super.provider); + + @override + SearchSuggestionType get type => + (origin as GetSearchSuggestionsProvider).type; + @override + String? get locationCountry => + (origin as GetSearchSuggestionsProvider).locationCountry; + @override + String? get locationState => + (origin as GetSearchSuggestionsProvider).locationState; + @override + String? get make => (origin as GetSearchSuggestionsProvider).make; + @override + String? get model => (origin as GetSearchSuggestionsProvider).model; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart deleted file mode 100644 index e220cc69f..000000000 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; - -import 'package:immich_mobile/modules/search/services/search.service.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; - -class SearchResultPageNotifier extends StateNotifier { - SearchResultPageNotifier(this._searchService) - : super( - SearchResultPageState( - searchResult: [], - isError: false, - isLoading: true, - isSuccess: false, - isSmart: false, - ), - ); - - final SearchService _searchService; - - Future search(String searchTerm, {bool smartSearch = true}) async { - state = state.copyWith( - searchResult: [], - isError: false, - isLoading: true, - isSuccess: false, - ); - - List? assets = - await _searchService.searchAsset(searchTerm, smartSearch: smartSearch); - - if (assets != null) { - state = state.copyWith( - searchResult: assets, - isError: false, - isLoading: false, - isSuccess: true, - isSmart: smartSearch, - ); - } else { - state = state.copyWith( - searchResult: [], - isError: true, - isLoading: false, - isSuccess: false, - isSmart: smartSearch, - ); - } - } -} - -final searchResultPageProvider = - StateNotifierProvider( - (ref) { - return SearchResultPageNotifier(ref.watch(searchServiceProvider)); -}); - -final searchRenderListProvider = Provider((ref) { - final result = ref.watch(searchResultPageProvider); - return ref.watch( - renderListProviderWithGrouping( - (result.searchResult, result.isSmart ? GroupAssetsBy.none : null), - ), - ); -}); diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart index 4f92e729f..884a01c9f 100644 --- a/mobile/lib/modules/search/services/person.service.dart +++ b/mobile/lib/modules/search/services/person.service.dart @@ -20,7 +20,7 @@ class PersonService { PersonService(this._apiService, this._db); - Future> getCuratedPeople() async { + Future> getAllPeople() async { try { final peopleResponseDto = await _apiService.personApi.getAllPeople(); return peopleResponseDto?.people ?? []; diff --git a/mobile/lib/modules/search/services/search.service.dart b/mobile/lib/modules/search/services/search.service.dart index 35249dec5..4d19657af 100644 --- a/mobile/lib/modules/search/services/search.service.dart +++ b/mobile/lib/modules/search/services/search.service.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; @@ -29,25 +30,92 @@ class SearchService { } } - Future?> searchAsset( - String searchTerm, { - bool smartSearch = true, + Future?> getSearchSuggestions( + SearchSuggestionType type, { + String? country, + String? state, + String? make, + String? model, }) async { - // TODO search in local DB: 1. when offline, 2. to find local assets try { - final SearchResponseDto? results = await _apiService.searchApi.search( - query: searchTerm, - smart: smartSearch, + return await _apiService.searchApi.getSearchSuggestions( + type, + country: country, + state: state, + make: make, + model: model, ); - if (results == null) { + } catch (e) { + debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}"); + return []; + } + } + + Future?> search(SearchFilter filter, int page) async { + try { + SearchResponseDto? response; + AssetTypeEnum? type; + if (filter.mediaType == AssetType.image) { + type = AssetTypeEnum.IMAGE; + } else if (filter.mediaType == AssetType.video) { + type = AssetTypeEnum.VIDEO; + } + + if (filter.context != null && filter.context!.isNotEmpty) { + response = await _apiService.searchApi.searchSmart( + SmartSearchDto( + query: filter.context!, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + isArchived: filter.display.isArchive, + isFavorite: filter.display.isFavorite, + isNotInAlbum: filter.display.isNotInAlbum, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } else { + response = await _apiService.searchApi.searchMetadata( + MetadataSearchDto( + originalFileName: + filter.filename != null && filter.filename!.isNotEmpty + ? filter.filename + : null, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + isArchived: filter.display.isArchive, + isFavorite: filter.display.isFavorite, + isNotInAlbum: filter.display.isNotInAlbum, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + if (response == null) { return null; } - // TODO local DB might be out of date; add assets not yet in DB? - return _db.assets.getAllByRemoteId(results.assets.items.map((e) => e.id)); - } catch (e) { - debugPrint("[ERROR] [searchAsset] ${e.toString()}"); - return null; + + return _db.assets + .getAllByRemoteId(response.assets.items.map((e) => e.id)); + } catch (error) { + debugPrint("Error [search] $error"); } + return null; } Future?> getCuratedLocation() async { diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart index 049c1ce46..a712c6929 100644 --- a/mobile/lib/modules/search/ui/curated_people_row.dart +++ b/mobile/lib/modules/search/ui/curated_people_row.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; @@ -23,7 +24,7 @@ class CuratedPeopleRow extends StatelessWidget { @override Widget build(BuildContext context) { - const imageSize = 80.0; + const imageSize = 60.0; // Guard empty [content] if (content.isEmpty) { @@ -82,11 +83,11 @@ class CuratedPeopleRow extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "Add name", + "exif_bottom_sheet_person_add_person", style: context.textTheme.labelLarge?.copyWith( color: context.primaryColor, ), - ), + ).tr(), ), ) else diff --git a/mobile/lib/modules/search/ui/explore_grid.dart b/mobile/lib/modules/search/ui/explore_grid.dart index fd49fff7c..ba55b5581 100644 --- a/mobile/lib/modules/search/ui/explore_grid.dart +++ b/mobile/lib/modules/search/ui/explore_grid.dart @@ -1,8 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -57,7 +59,22 @@ class ExploreGrid extends StatelessWidget { ), ) : context.pushRoute( - SearchResultRoute(searchTerm: 'm:${content.label}'), + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: content.label, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), ); }, ); diff --git a/mobile/lib/modules/search/ui/immich_search_bar.dart b/mobile/lib/modules/search/ui/immich_search_bar.dart deleted file mode 100644 index f4fa62d26..000000000 --- a/mobile/lib/modules/search/ui/immich_search_bar.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; - -class ImmichSearchBar extends HookConsumerWidget - implements PreferredSizeWidget { - const ImmichSearchBar({ - super.key, - required this.searchFocusNode, - required this.onSubmitted, - }); - - final FocusNode searchFocusNode; - final Function(String) onSubmitted; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTermController = useTextEditingController(text: ""); - final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; - - focusSearch() { - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms(); - ref.watch(searchPageStateProvider.notifier).enableSearch(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - - searchFocusNode.requestFocus(); - } - - useEffect( - () { - searchFocusNotifier.addListener(focusSearch); - return () { - searchFocusNotifier.removeListener(focusSearch); - }; - }, - [], - ); - - return AppBar( - automaticallyImplyLeading: false, - leading: isSearchEnabled - ? IconButton( - onPressed: () { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - searchTermController.clear(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ) - : const Icon( - Icons.search_rounded, - size: 20, - ), - title: TextField( - controller: searchTermController, - focusNode: searchFocusNode, - autofocus: false, - onTap: focusSearch, - onSubmitted: (searchTerm) { - onSubmitted(searchTerm); - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - }, - onChanged: (value) { - ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); - }, - decoration: InputDecoration( - hintText: 'search_bar_hint'.tr(), - hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurface.withOpacity(0.75), - ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - ), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -// Used to focus search from outside this widget. -// For example when double pressing the search nav icon. -final searchFocusNotifier = SearchFocusNotifier(); - -class SearchFocusNotifier with ChangeNotifier { - void requestFocus() { - notifyListeners(); - } -} diff --git a/mobile/lib/modules/search/ui/search_filter/camera_picker.dart b/mobile/lib/modules/search/ui/search_filter/camera_picker.dart new file mode 100644 index 000000000..fdfd398e6 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/camera_picker.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart'; +import 'package:openapi/api.dart'; + +class CameraPicker extends HookConsumerWidget { + const CameraPicker({super.key, required this.onSelect, this.filter}); + + final Function(Map) onSelect; + final SearchCameraFilter? filter; + @override + Widget build(BuildContext context, WidgetRef ref) { + final makeTextController = useTextEditingController(text: filter?.make); + final modelTextController = useTextEditingController(text: filter?.model); + final selectedMake = useState(filter?.make); + final selectedModel = useState(filter?.model); + + final make = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.cameraMake, + ), + ); + + final models = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.cameraModel, + make: selectedMake.value, + ), + ); + + final inputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + contentPadding: const EdgeInsets.only(left: 16), + ); + + final menuStyle = MenuStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ); + + return Container( + padding: const EdgeInsets.only( + // bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + DropdownMenu( + dropdownMenuEntries: switch (make) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + width: context.width * 0.45, + menuHeight: 400, + label: const Text('Make'), + inputDecorationTheme: inputDecorationTheme, + controller: makeTextController, + menuStyle: menuStyle, + leadingIcon: const Icon(Icons.photo_camera_rounded), + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedMake.value = value.toString(); + onSelect({ + 'make': selectedMake.value, + 'model': selectedModel.value, + }); + }, + ), + DropdownMenu( + dropdownMenuEntries: switch (models) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + width: context.width * 0.45, + menuHeight: 400, + label: const Text('Model'), + inputDecorationTheme: inputDecorationTheme, + controller: modelTextController, + menuStyle: menuStyle, + leadingIcon: const Icon(Icons.camera), + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedModel.value = value.toString(); + onSelect({ + 'make': selectedMake.value, + 'model': selectedModel.value, + }); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart b/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart new file mode 100644 index 000000000..f6cd01cbb --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; + +enum DisplayOption { + notInAlbum, + favorite, + archive, +} + +class DisplayOptionPicker extends HookWidget { + const DisplayOptionPicker({ + super.key, + required this.onSelect, + this.filter, + }); + + final Function(Map) onSelect; + final SearchDisplayFilters? filter; + + @override + Widget build(BuildContext context) { + final options = useState>({ + DisplayOption.notInAlbum: filter?.isNotInAlbum ?? false, + DisplayOption.favorite: filter?.isFavorite ?? false, + DisplayOption.archive: filter?.isArchive ?? false, + }); + + return ListView( + shrinkWrap: true, + children: [ + CheckboxListTile( + title: const Text('Not in album'), + value: options.value[DisplayOption.notInAlbum], + onChanged: (bool? value) { + options.value = { + ...options.value, + DisplayOption.notInAlbum: value!, + }; + onSelect(options.value); + }, + ), + CheckboxListTile( + title: const Text('Favorite'), + value: options.value[DisplayOption.favorite], + onChanged: (value) { + options.value = { + ...options.value, + DisplayOption.favorite: value!, + }; + onSelect(options.value); + }, + ), + CheckboxListTile( + title: const Text('Archive'), + value: options.value[DisplayOption.archive], + onChanged: (value) { + options.value = { + ...options.value, + DisplayOption.archive: value!, + }; + onSelect(options.value); + }, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart new file mode 100644 index 000000000..46bfe96bb --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class FilterBottomSheetScaffold extends StatelessWidget { + const FilterBottomSheetScaffold({ + super.key, + required this.child, + required this.onSearch, + required this.onClear, + required this.title, + this.expanded, + }); + + final bool? expanded; + final String title; + final Widget child; + final Function() onSearch; + final Function() onClear; + + @override + Widget build(BuildContext context) { + buildChildWidget() { + if (expanded != null && expanded == true) { + return Expanded(child: child); + } + return child; + } + + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: context.textTheme.headlineSmall, + ), + ), + buildChildWidget(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () { + onClear(); + Navigator.of(context).pop(); + }, + child: const Text('Clear'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () { + onSearch(); + Navigator.of(context).pop(); + }, + child: const Text('Apply filter'), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/location_picker.dart b/mobile/lib/modules/search/ui/search_filter/location_picker.dart new file mode 100644 index 000000000..22568da47 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/location_picker.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart'; +import 'package:openapi/api.dart'; + +class LocationPicker extends HookConsumerWidget { + const LocationPicker({super.key, required this.onSelected, this.filter}); + + final Function(Map) onSelected; + final SearchLocationFilter? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final countryTextController = + useTextEditingController(text: filter?.country); + final stateTextController = useTextEditingController(text: filter?.state); + final cityTextController = useTextEditingController(text: filter?.city); + + final selectedCountry = useState(filter?.country); + final selectedState = useState(filter?.state); + final selectedCity = useState(filter?.city); + + final countries = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.country, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final states = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.state, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final cities = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.city, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final inputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + contentPadding: const EdgeInsets.only(left: 16), + ); + + final menuStyle = MenuStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ); + + return Column( + children: [ + DropdownMenu( + dropdownMenuEntries: switch (countries) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('Country'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: countryTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedCountry.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + const SizedBox( + height: 16, + ), + DropdownMenu( + dropdownMenuEntries: switch (states) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('State'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: stateTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedState.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + const SizedBox( + height: 16, + ), + DropdownMenu( + dropdownMenuEntries: switch (cities) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('City'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: cityTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedCity.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart b/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart new file mode 100644 index 000000000..aaef2c815 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; + +class MediaTypePicker extends HookWidget { + const MediaTypePicker({super.key, required this.onSelect, this.filter}); + + final Function(AssetType) onSelect; + final AssetType? filter; + + @override + Widget build(BuildContext context) { + final selectedMediaType = useState(filter ?? AssetType.other); + + return ListView( + shrinkWrap: true, + children: [ + RadioListTile( + title: const Text("All"), + value: AssetType.other, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + RadioListTile( + title: const Text("Image"), + value: AssetType.image, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + RadioListTile( + title: const Text("Video"), + value: AssetType.video, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/people_picker.dart b/mobile/lib/modules/search/ui/search_filter/people_picker.dart new file mode 100644 index 000000000..74aad06b8 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/people_picker.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/shared/models/store.dart' as local_store; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; + +class PeoplePicker extends HookConsumerWidget { + const PeoplePicker({super.key, required this.onSelect, this.filter}); + + final Function(Set) onSelect; + final Set? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + var imageSize = 45.0; + final people = ref.watch(getAllPeopleProvider); + final headers = { + "x-immich-user-token": + local_store.Store.get(local_store.StoreKey.accessToken), + }; + final selectedPeople = useState>(filter ?? {}); + + return people.widgetWhen( + onData: (people) { + return ListView.builder( + shrinkWrap: true, + itemCount: people.length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final person = people[index]; + return Card( + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + child: ListTile( + title: Text( + person.name, + style: context.textTheme.bodyLarge, + ), + leading: SizedBox( + height: imageSize, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: imageSize / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), + ), + ), + ), + onTap: () { + if (selectedPeople.value.contains(person)) { + selectedPeople.value.remove(person); + } else { + selectedPeople.value.add(person); + } + + selectedPeople.value = {...selectedPeople.value}; + onSelect(selectedPeople.value); + }, + selected: selectedPeople.value.contains(person), + selectedTileColor: context.primaryColor.withOpacity(0.2), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart b/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart new file mode 100644 index 000000000..b2e0d086a --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SearchFilterChip extends StatelessWidget { + final String label; + final Function() onTap; + final Widget? currentFilter; + final IconData icon; + + const SearchFilterChip({ + super.key, + required this.label, + required this.onTap, + required this.icon, + this.currentFilter, + }); + + @override + Widget build(BuildContext context) { + if (currentFilter != null) { + return GestureDetector( + onTap: onTap, + child: Card( + elevation: 0, + color: context.primaryColor.withAlpha(25), + shape: StadiumBorder( + side: BorderSide(color: context.primaryColor), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), + child: Row( + children: [ + Icon( + icon, + size: 18, + ), + const SizedBox(width: 4.0), + currentFilter!, + ], + ), + ), + ), + ); + } + return GestureDetector( + onTap: onTap, + child: Card( + elevation: 0, + shape: + StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), + child: Row( + children: [ + Icon( + icon, + size: 18, + ), + const SizedBox(width: 4.0), + Text(label), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart b/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart new file mode 100644 index 000000000..57545413d --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +Future showFilterBottomSheet({ + required BuildContext context, + required Widget child, + bool isScrollControlled = false, + bool isDismissible = true, +}) async { + return await showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + useSafeArea: false, + isDismissible: isDismissible, + showDragHandle: isDismissible, + builder: (BuildContext context) { + return child; + }, + ); +} diff --git a/mobile/lib/modules/search/ui/search_suggestion_list.dart b/mobile/lib/modules/search/ui/search_suggestion_list.dart deleted file mode 100644 index c9694eb75..000000000 --- a/mobile/lib/modules/search/ui/search_suggestion_list.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; - -class SearchSuggestionList extends ConsumerWidget { - const SearchSuggestionList({super.key, required this.onSubmitted}); - - final Function(String) onSubmitted; - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTerm = ref.watch(searchPageStateProvider).searchTerm; - final searchSuggestion = - ref.watch(searchPageStateProvider).searchSuggestion; - - return Container( - color: searchTerm.isEmpty - ? Colors.black.withOpacity(0.5) - : context.scaffoldBackgroundColor, - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Container( - color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'search_suggestion_list_smart_search_hint_1'.tr(), - style: context.textTheme.bodyMedium, - ), - TextSpan( - text: 'search_suggestion_list_smart_search_hint_2'.tr(), - style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ), - ), - SliverFillRemaining( - hasScrollBody: true, - child: ListView.builder( - itemBuilder: ((context, index) { - return ListTile( - onTap: () { - onSubmitted("m:${searchSuggestion[index]}"); - }, - title: Text(searchSuggestion[index]), - ); - }), - itemCount: searchSuggestion.length, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart index 1f90922c1..3414edc05 100644 --- a/mobile/lib/modules/search/views/all_people_page.dart +++ b/mobile/lib/modules/search/views/all_people_page.dart @@ -1,35 +1,38 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/modules/search/providers/people.provider.dart'; -import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getCuratedPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text( - 'all_people_page_title', - ).tr(), - leading: IconButton( - onPressed: () => context.popRoute(), - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people, - ), - ), - ); - } -} +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; + +@RoutePage() +class AllPeoplePage extends HookConsumerWidget { + const AllPeoplePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final curatedPeople = ref.watch(getAllPeopleProvider); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'all_people_page_title', + ).tr(), + leading: IconButton( + onPressed: () => context.popRoute(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + ), + body: curatedPeople.widgetWhen( + onData: (people) => ExploreGrid( + isPeople: true, + curatedContent: people + .map((e) => CuratedContent(label: e.name, id: e.id)) + .toList(), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_input_page.dart b/mobile/lib/modules/search/views/search_input_page.dart new file mode 100644 index 000000000..a35341606 --- /dev/null +++ b/mobile/lib/modules/search/views/search_input_page.dart @@ -0,0 +1,563 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/paginated_search.provider.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/camera_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/display_option_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/location_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/media_type_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/people_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_chip.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_utils.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; +import 'package:openapi/api.dart'; + +@RoutePage() +class SearchInputPage extends HookConsumerWidget { + const SearchInputPage({super.key, this.prefilter}); + + final SearchFilter? prefilter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isContextualSearch = useState(true); + final textSearchController = useTextEditingController(); + final filter = useState( + SearchFilter( + people: prefilter?.people ?? {}, + location: prefilter?.location ?? SearchLocationFilter(), + camera: prefilter?.camera ?? SearchCameraFilter(), + date: prefilter?.date ?? SearchDateFilter(), + display: prefilter?.display ?? + SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: prefilter?.mediaType ?? AssetType.other, + ), + ); + + final previousFilter = useState(filter.value); + + final peopleCurrentFilterWidget = useState(null); + final dateRangeCurrentFilterWidget = useState(null); + final cameraCurrentFilterWidget = useState(null); + final locationCurrentFilterWidget = useState(null); + final mediaTypeCurrentFilterWidget = useState(null); + final displayOptionCurrentFilterWidget = useState(null); + + final currentPage = useState(1); + final searchProvider = ref.watch(paginatedSearchProvider); + final searchResultCount = useState(0); + + search() async { + if (prefilter == null && filter.value == previousFilter.value) return; + + ref.watch(paginatedSearchProvider.notifier).clear(); + + currentPage.value = 1; + + final searchResult = await ref + .watch(paginatedSearchProvider.notifier) + .getNextPage(filter.value, currentPage.value); + previousFilter.value = filter.value; + + searchResultCount.value = searchResult.length; + } + + searchPrefilter() { + if (prefilter != null) { + Future.delayed( + Duration.zero, + () { + search(); + + if (prefilter!.location.city != null) { + locationCurrentFilterWidget.value = Text( + prefilter!.location.city!, + style: context.textTheme.labelLarge, + ); + } + }, + ); + } + } + + useEffect( + () { + searchPrefilter(); + return null; + }, + [], + ); + + loadMoreSearchResult() async { + currentPage.value += 1; + final searchResult = await ref + .watch(paginatedSearchProvider.notifier) + .getNextPage(filter.value, currentPage.value); + searchResultCount.value = searchResult.length; + } + + showPeoplePicker() { + handleOnSelect(Set value) { + filter.value = filter.value.copyWith( + people: value, + ); + + peopleCurrentFilterWidget.value = Text( + value.map((e) => e.name != '' ? e.name : "No name").join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + people: {}, + ); + + peopleCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'Select people', + expanded: true, + onSearch: search, + onClear: handleClear, + child: PeoplePicker( + onSelect: handleOnSelect, + filter: filter.value.people, + ), + ), + ), + ); + } + + showLocationPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + location: SearchLocationFilter( + country: value['country'], + city: value['city'], + state: value['state'], + ), + ); + + final locationText = []; + if (value['country'] != null) { + locationText.add(value['country']!); + } + + if (value['state'] != null) { + locationText.add(value['state']!); + } + + if (value['city'] != null) { + locationText.add(value['city']!); + } + + locationCurrentFilterWidget.value = Text( + locationText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + location: SearchLocationFilter(), + ); + + locationCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + child: FilterBottomSheetScaffold( + title: 'Select location', + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: LocationPicker( + onSelected: handleOnSelect, + filter: filter.value.location, + ), + ), + ), + ), + ); + } + + showCameraPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter( + make: value['make'], + model: value['model'], + ), + ); + + cameraCurrentFilterWidget.value = Text( + '${value['make'] ?? ''} ${value['model'] ?? ''}', + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter(), + ); + + cameraCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + child: FilterBottomSheetScaffold( + title: 'Select camera type', + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: CameraPicker( + onSelect: handleOnSelect, + filter: filter.value.camera, + ), + ), + ), + ); + } + + showDatePicker() async { + final firstDate = DateTime(1900); + final lastDate = DateTime.now(); + + final date = await showDateRangePicker( + context: context, + firstDate: firstDate, + lastDate: lastDate, + currentDate: DateTime.now(), + initialDateRange: DateTimeRange( + start: filter.value.date.takenAfter ?? lastDate, + end: filter.value.date.takenBefore ?? lastDate, + ), + helpText: 'Select a date range', + cancelText: 'Cancel', + confirmText: 'Select', + saveText: 'Save', + errorFormatText: 'Invalid date format', + errorInvalidText: 'Invalid date', + fieldStartHintText: 'Start date', + fieldEndHintText: 'End date', + initialEntryMode: DatePickerEntryMode.input, + ); + + if (date == null) { + filter.value = filter.value.copyWith( + date: SearchDateFilter(), + ); + + dateRangeCurrentFilterWidget.value = null; + search(); + return; + } + + filter.value = filter.value.copyWith( + date: SearchDateFilter( + takenAfter: date.start, + takenBefore: date.end.add( + const Duration( + hours: 23, + minutes: 59, + seconds: 59, + ), + ), + ), + ); + + // If date range is less than 24 hours, set the end date to the end of the day + if (date.end.difference(date.start).inHours < 24) { + dateRangeCurrentFilterWidget.value = Text( + date.start.toLocal().toIso8601String().split('T').first, + style: context.textTheme.labelLarge, + ); + } else { + dateRangeCurrentFilterWidget.value = Text( + '${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}', + style: context.textTheme.labelLarge, + ); + } + + search(); + } + + // MEDIA PICKER + showMediaTypePicker() { + handleOnSelected(AssetType assetType) { + filter.value = filter.value.copyWith( + mediaType: assetType, + ); + + mediaTypeCurrentFilterWidget.value = Text( + assetType == AssetType.image ? 'Image' : 'Video', + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + mediaType: AssetType.other, + ); + + mediaTypeCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'Select media type', + onSearch: search, + onClear: handleClear, + child: MediaTypePicker( + onSelect: handleOnSelected, + filter: filter.value.mediaType, + ), + ), + ); + } + + // DISPLAY OPTION + showDisplayOptionPicker() { + handleOnSelect(Map value) { + final filterText = []; + + value.forEach((key, value) { + switch (key) { + case DisplayOption.notInAlbum: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isNotInAlbum: value, + ), + ); + if (value) filterText.add('Not in album'); + break; + case DisplayOption.archive: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isArchive: value, + ), + ); + if (value) filterText.add('Archive'); + break; + case DisplayOption.favorite: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isFavorite: value, + ), + ); + if (value) filterText.add('Favorite'); + break; + } + }); + + displayOptionCurrentFilterWidget.value = Text( + filterText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + ); + + displayOptionCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'Display options', + onSearch: search, + onClear: handleClear, + child: DisplayOptionPicker( + onSelect: handleOnSelect, + filter: filter.value.display, + ), + ), + ); + } + + handleTextSubmitted(String value) { + if (isContextualSearch.value) { + filter.value = filter.value.copyWith( + context: value, + filename: null, + ); + } else { + filter.value = filter.value.copyWith(filename: value, context: null); + } + + search(); + } + + buildSearchResult() { + return switch (searchProvider) { + AsyncData() => Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + final shouldLoadMore = searchResultCount.value > 75; + if (metrics.pixels >= metrics.maxScrollExtent && + shouldLoadMore) { + loadMoreSearchResult(); + } + return true; + }, + child: MultiselectGrid( + renderListProvider: paginatedSearchRenderListProvider, + archiveEnabled: true, + deleteEnabled: true, + editEnabled: true, + favoriteEnabled: true, + stackEnabled: false, + emptyIndicator: const SizedBox(), + ), + ), + ), + ), + AsyncError(:final error) => Text('Error: $error'), + _ => const Expanded(child: Center(child: CircularProgressIndicator())), + }; + } + + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + automaticallyImplyLeading: true, + actions: [ + IconButton( + icon: isContextualSearch.value + ? const Icon(Icons.abc_rounded) + : const Icon(Icons.image_search_rounded), + onPressed: () { + isContextualSearch.value = !isContextualSearch.value; + textSearchController.clear(); + }, + ), + ], + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + onPressed: () { + context.router.pop(); + }, + ), + title: TextField( + controller: textSearchController, + decoration: InputDecoration( + hintText: isContextualSearch.value + ? 'Sunrise on the beach' + : 'File name or extension', + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + fontWeight: FontWeight.w500, + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + ), + onSubmitted: handleTextSubmitted, + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: SizedBox( + height: 50, + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SearchFilterChip( + icon: Icons.people_alt_rounded, + onTap: showPeoplePicker, + label: 'People', + currentFilter: peopleCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.location_pin, + onTap: showLocationPicker, + label: 'Location', + currentFilter: locationCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.camera_alt_rounded, + onTap: showCameraPicker, + label: 'Camera', + currentFilter: cameraCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.date_range_rounded, + onTap: showDatePicker, + label: 'Date', + currentFilter: dateRangeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.video_collection_outlined, + onTap: showMediaTypePicker, + label: 'Media Type', + currentFilter: mediaTypeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.display_settings_outlined, + onTap: showDisplayOptionPicker, + label: 'Display Options', + currentFilter: displayOptionCurrentFilterWidget.value, + ), + ], + ), + ), + ), + buildSearchResult(), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index ab114d691..27ca28126 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -1,279 +1,274 @@ -import 'dart:math' as math; -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; -import 'package:immich_mobile/modules/search/providers/people.provider.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; -import 'package:immich_mobile/modules/search/ui/curated_people_row.dart'; -import 'package:immich_mobile/modules/search/ui/curated_places_row.dart'; -import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart'; -import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; -import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; -import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; - -@RoutePage() -// ignore: must_be_immutable -class SearchPage extends HookConsumerWidget { - SearchPage({super.key}); - - FocusNode searchFocusNode = FocusNode(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; - final curatedLocation = ref.watch(getCuratedLocationProvider); - final curatedPeople = ref.watch(getCuratedPeopleProvider); - final isMapEnabled = - ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); - double imageSize = math.min(context.width / 3, 150); - - TextStyle categoryTitleStyle = const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 15.0, - ); - - Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; - - useEffect( - () { - searchFocusNode = FocusNode(); - return () => searchFocusNode.dispose(); - }, - [], - ); - - onSearchSubmitted(String searchTerm) async { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - - context.pushRoute( - SearchResultRoute( - searchTerm: searchTerm, - ), - ); - } - - showNameEditModel( - String personId, - String personName, - ) { - return showDialog( - context: context, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - buildPeople() { - return SizedBox( - height: imageSize, - child: curatedPeople.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (people) => Padding( - padding: const EdgeInsets.only( - left: 16, - top: 8, - ), - child: CuratedPeopleRow( - content: people.take(12).toList(), - onTap: (content, index) { - context.pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ); - }, - onNameTap: (person, index) => { - showNameEditModel(person.id, person.label), - }, - ), - ), - ), - ); - } - - buildPlaces() { - return SizedBox( - height: imageSize, - child: curatedLocation.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (locations) => CuratedPlacesRow( - isMapEnabled: isMapEnabled, - content: locations - .map( - (o) => CuratedContent( - id: o.id, - label: o.city, - ), - ) - .toList(), - imageSize: imageSize, - onTap: (content, index) { - context.pushRoute( - SearchResultRoute( - searchTerm: 'm:${content.label}', - ), - ); - }, - ), - ), - ); - } - - return Scaffold( - appBar: ImmichSearchBar( - searchFocusNode: searchFocusNode, - onSubmitted: onSearchSubmitted, - ), - body: GestureDetector( - onTap: () { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - }, - child: Stack( - children: [ - ListView( - children: [ - SearchRowTitle( - title: "search_page_people".tr(), - onViewAllPressed: () => - context.pushRoute(const AllPeopleRoute()), - ), - buildPeople(), - SearchRowTitle( - title: "search_page_places".tr(), - onViewAllPressed: () => - context.pushRoute(const CuratedLocationRoute()), - top: 0, - ), - const SizedBox(height: 10.0), - buildPlaces(), - const SizedBox(height: 24.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'search_page_your_activity', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ListTile( - leading: Icon( - Icons.favorite_border_rounded, - color: categoryIconColor, - ), - title: - Text('search_page_favorites', style: categoryTitleStyle) - .tr(), - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - const CategoryDivider(), - ListTile( - leading: Icon( - Icons.schedule_outlined, - color: categoryIconColor, - ), - title: Text( - 'search_page_recently_added', - style: categoryTitleStyle, - ).tr(), - onTap: () => context.pushRoute(const RecentlyAddedRoute()), - ), - const SizedBox(height: 24.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'search_page_categories', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ListTile( - title: - Text('search_page_screenshots', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.screenshot, - color: categoryIconColor, - ), - onTap: () => context.pushRoute( - SearchResultRoute( - searchTerm: 'screenshots', - ), - ), - ), - const CategoryDivider(), - ListTile( - title: Text('search_page_selfies', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.photo_camera_front_outlined, - color: categoryIconColor, - ), - onTap: () => context.pushRoute( - SearchResultRoute( - searchTerm: 'selfies', - ), - ), - ), - const CategoryDivider(), - ListTile( - title: Text('search_page_videos', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.play_circle_outline, - color: categoryIconColor, - ), - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - const CategoryDivider(), - ListTile( - title: Text( - 'search_page_motion_photos', - style: categoryTitleStyle, - ).tr(), - leading: Icon( - Icons.motion_photos_on_outlined, - color: categoryIconColor, - ), - onTap: () => context.pushRoute(const AllMotionPhotosRoute()), - ), - ], - ), - if (isSearchEnabled) - SearchSuggestionList(onSubmitted: onSearchSubmitted), - ], - ), - ), - ); - } -} - -class CategoryDivider extends StatelessWidget { - const CategoryDivider({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only( - left: 56, - right: 16, - ), - child: Divider( - height: 0, - ), - ); - } -} +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; +import 'package:immich_mobile/modules/search/ui/curated_people_row.dart'; +import 'package:immich_mobile/modules/search/ui/curated_places_row.dart'; +import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; +import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/server_info.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; + +@RoutePage() +// ignore: must_be_immutable +class SearchPage extends HookConsumerWidget { + const SearchPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final curatedLocation = ref.watch(getCuratedLocationProvider); + final curatedPeople = ref.watch(getAllPeopleProvider); + final isMapEnabled = + ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); + double imageSize = math.min(context.width / 3, 150); + + TextStyle categoryTitleStyle = const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15.0, + ); + + Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; + + showNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ); + } + + buildPeople() { + return SizedBox( + height: imageSize, + child: curatedPeople.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (people) => Padding( + padding: const EdgeInsets.only( + left: 16, + top: 8, + ), + child: CuratedPeopleRow( + content: people + .map((e) => CuratedContent(label: e.name, id: e.id)) + .take(12) + .toList(), + onTap: (content, index) { + context.pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ); + }, + onNameTap: (person, index) => { + showNameEditModel(person.id, person.label), + }, + ), + ), + ), + ); + } + + buildPlaces() { + return SizedBox( + height: imageSize, + child: curatedLocation.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (locations) => CuratedPlacesRow( + isMapEnabled: isMapEnabled, + content: locations + .map( + (o) => CuratedContent( + id: o.id, + label: o.city, + ), + ) + .toList(), + imageSize: imageSize, + onTap: (content, index) { + context.pushRoute( + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: content.label, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), + ); + }, + ), + ), + ); + } + + buildSearchButton() { + return GestureDetector( + onTap: () { + context.pushRoute(SearchInputRoute()); + }, + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.isDarkTheme + ? Colors.grey[800]! + : const Color.fromARGB(255, 225, 225, 225), + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: Row( + children: [ + Icon(Icons.search, color: context.primaryColor), + const SizedBox(width: 16.0), + Text( + "Search your photos", + style: context.textTheme.bodyLarge?.copyWith( + color: + context.isDarkTheme ? Colors.white70 : Colors.black54, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ); + } + + return Scaffold( + appBar: const ImmichAppBar(), + body: Stack( + children: [ + ListView( + children: [ + buildSearchButton(), + SearchRowTitle( + title: "search_page_people".tr(), + onViewAllPressed: () => + context.pushRoute(const AllPeopleRoute()), + ), + buildPeople(), + SearchRowTitle( + title: "search_page_places".tr(), + onViewAllPressed: () => + context.pushRoute(const CuratedLocationRoute()), + top: 0, + ), + const SizedBox(height: 10.0), + buildPlaces(), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'search_page_your_activity', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ), + ListTile( + leading: Icon( + Icons.favorite_border_rounded, + color: categoryIconColor, + ), + title: Text('search_page_favorites', style: categoryTitleStyle) + .tr(), + onTap: () => context.pushRoute(const FavoritesRoute()), + ), + const CategoryDivider(), + ListTile( + leading: Icon( + Icons.schedule_outlined, + color: categoryIconColor, + ), + title: Text( + 'search_page_recently_added', + style: categoryTitleStyle, + ).tr(), + onTap: () => context.pushRoute(const RecentlyAddedRoute()), + ), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'search_page_categories', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ), + ListTile( + title: + Text('search_page_videos', style: categoryTitleStyle).tr(), + leading: Icon( + Icons.play_circle_outline, + color: categoryIconColor, + ), + onTap: () => context.pushRoute(const AllVideosRoute()), + ), + const CategoryDivider(), + ListTile( + title: Text( + 'search_page_motion_photos', + style: categoryTitleStyle, + ).tr(), + leading: Icon( + Icons.motion_photos_on_outlined, + color: categoryIconColor, + ), + onTap: () => context.pushRoute(const AllMotionPhotosRoute()), + ), + ], + ), + ], + ), + ); + } +} + +class CategoryDivider extends StatelessWidget { + const CategoryDivider({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.only( + left: 56, + right: 16, + ), + child: Divider( + height: 0, + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart deleted file mode 100644 index 97df5f10c..000000000 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ /dev/null @@ -1,213 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; -import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; -import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; - -class SearchType { - SearchType({required this.isSmart, required this.searchTerm}); - - final bool isSmart; - final String searchTerm; -} - -SearchType _getSearchType(String searchTerm) { - if (searchTerm.startsWith('m:')) { - return SearchType(isSmart: false, searchTerm: searchTerm.substring(2)); - } else { - return SearchType(isSmart: true, searchTerm: searchTerm); - } -} - -@RoutePage() -class SearchResultPage extends HookConsumerWidget { - const SearchResultPage({ - super.key, - required this.searchTerm, - }); - - final String searchTerm; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTermController = useTextEditingController(text: ""); - final isNewSearch = useState(false); - final currentSearchTerm = useState(searchTerm); - - FocusNode? searchFocusNode; - - useEffect( - () { - searchFocusNode = FocusNode(); - - var searchType = _getSearchType(searchTerm); - - Future.delayed( - Duration.zero, - () => ref - .read(searchResultPageProvider.notifier) - .search(searchType.searchTerm, smartSearch: searchType.isSmart), - ); - return () => searchFocusNode?.dispose(); - }, - [], - ); - - Future onSearchSubmitted(String newSearchTerm) { - debugPrint("Re-Search with $newSearchTerm"); - searchFocusNode?.unfocus(); - isNewSearch.value = false; - currentSearchTerm.value = newSearchTerm; - var searchType = _getSearchType(newSearchTerm); - return ref - .watch(searchResultPageProvider.notifier) - .search(searchType.searchTerm, smartSearch: searchType.isSmart); - } - - buildTextField() { - return TextField( - controller: searchTermController, - focusNode: searchFocusNode, - autofocus: false, - onTap: () { - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - searchFocusNode?.requestFocus(); - }, - textInputAction: TextInputAction.search, - onSubmitted: (searchTerm) { - if (searchTerm.isNotEmpty) { - searchTermController.clear(); - onSearchSubmitted(searchTerm); - } else { - isNewSearch.value = false; - } - }, - onChanged: (value) { - ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); - }, - decoration: InputDecoration( - hintText: 'search_result_page_new_search_hint'.tr(), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - hintStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16.0, - color: context.isDarkTheme - ? Colors.grey[500] - : Colors.black.withOpacity(0.5), - ), - ), - ); - } - - buildChip() { - return Chip( - label: Wrap( - spacing: 5, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - children: [ - Text( - currentSearchTerm.value, - style: TextStyle( - color: context.primaryColor, - fontSize: 13, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - ), - Icon( - Icons.close_rounded, - color: context.primaryColor, - size: 20, - ), - ], - ), - backgroundColor: context.primaryColor.withAlpha(50), - ); - } - - Future refresh() async => onSearchSubmitted(currentSearchTerm.value); - - buildSearchResult() { - final searchResultPageState = ref.watch(searchResultPageProvider); - - if (searchResultPageState.isError) { - return Padding( - padding: const EdgeInsets.all(12), - child: const Text("common_server_error").tr(), - ); - } - - if (searchResultPageState.isLoading) { - return const Center(child: ImmichLoadingIndicator()); - } - - if (searchResultPageState.isSuccess) { - return MultiselectGrid( - renderListProvider: searchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - onRefresh: refresh, - ); - } - - return const SizedBox(); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - splashRadius: 20, - onPressed: () { - if (isNewSearch.value) { - isNewSearch.value = false; - } else { - context.popRoute(true); - } - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - title: GestureDetector( - onTap: () { - isNewSearch.value = true; - searchFocusNode?.requestFocus(); - }, - child: isNewSearch.value ? buildTextField() : buildChip(), - ), - centerTitle: false, - ), - body: GestureDetector( - onTap: () { - if (searchFocusNode != null) { - searchFocusNode?.unfocus(); - } - - ref.watch(searchPageStateProvider.notifier).disableSearch(); - }, - child: Stack( - children: [ - buildSearchResult(), - if (isNewSearch.value) - SearchSuggestionList(onSubmitted: onSearchSubmitted), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index f5c1a95d9..46cd7522d 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -31,7 +31,9 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart'; +import 'package:immich_mobile/modules/search/views/search_input_page.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart'; @@ -43,7 +45,6 @@ import 'package:immich_mobile/modules/search/views/curated_location_page.dart'; import 'package:immich_mobile/modules/search/views/person_result_page.dart'; import 'package:immich_mobile/modules/search/views/recently_added_page.dart'; import 'package:immich_mobile/modules/search/views/search_page.dart'; -import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/settings/views/settings_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/custom_transition_builders.dart'; @@ -125,10 +126,6 @@ class AppRouter extends _$AppRouter { page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard], ), - AutoRoute( - page: SearchResultRoute.page, - guards: [_authGuard, _duplicateGuard], - ), AutoRoute( page: CuratedLocationRoute.page, guards: [_authGuard, _duplicateGuard], @@ -223,6 +220,11 @@ class AppRouter extends _$AppRouter { page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard], ), + CustomRoute( + page: SearchInputRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.noTransition, + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index cc86b701a..fa9fa32f6 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -255,22 +255,21 @@ abstract class _$AppRouter extends RootStackRouter { child: const RecentlyAddedPage(), ); }, - SearchRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SearchRouteArgs()); + SearchInputRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const SearchInputRouteArgs()); return AutoRoutePage( routeData: routeData, - child: SearchPage(key: args.key), + child: SearchInputPage( + key: args.key, + prefilter: args.prefilter, + ), ); }, - SearchResultRoute.name: (routeData) { - final args = routeData.argsAs(); + SearchRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: SearchResultPage( - key: args.key, - searchTerm: args.searchTerm, - ), + child: const SearchPage(), ); }, SelectAdditionalUserForSharingRoute.name: (routeData) { @@ -1113,69 +1112,55 @@ class RecentlyAddedRoute extends PageRouteInfo { } /// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ +/// [SearchInputPage] +class SearchInputRoute extends PageRouteInfo { + SearchInputRoute({ Key? key, + SearchFilter? prefilter, List? children, }) : super( + SearchInputRoute.name, + args: SearchInputRouteArgs( + key: key, + prefilter: prefilter, + ), + initialChildren: children, + ); + + static const String name = 'SearchInputRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class SearchInputRouteArgs { + const SearchInputRouteArgs({ + this.key, + this.prefilter, + }); + + final Key? key; + + final SearchFilter? prefilter; + + @override + String toString() { + return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}'; + } +} + +/// generated route for +/// [SearchPage] +class SearchRoute extends PageRouteInfo { + const SearchRoute({List? children}) + : super( SearchRoute.name, - args: SearchRouteArgs(key: key), initialChildren: children, ); static const String name = 'SearchRoute'; - static const PageInfo page = PageInfo(name); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key}); - - final Key? key; - - @override - String toString() { - return 'SearchRouteArgs{key: $key}'; - } -} - -/// generated route for -/// [SearchResultPage] -class SearchResultRoute extends PageRouteInfo { - SearchResultRoute({ - Key? key, - required String searchTerm, - List? children, - }) : super( - SearchResultRoute.name, - args: SearchResultRouteArgs( - key: key, - searchTerm: searchTerm, - ), - initialChildren: children, - ); - - static const String name = 'SearchResultRoute'; - - static const PageInfo page = - PageInfo(name); -} - -class SearchResultRouteArgs { - const SearchResultRouteArgs({ - this.key, - required this.searchTerm, - }); - - final Key? key; - - final String searchTerm; - - @override - String toString() { - return 'SearchResultRouteArgs{key: $key, searchTerm: $searchTerm}'; - } + static const PageInfo page = PageInfo(name); } /// generated route for diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index dafbedd31..afe87fb24 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -38,7 +38,7 @@ class TabNavigationObserver extends AutoRouterObserver { if (route.name == 'SearchRoute') { // Refresh Location State ref.invalidate(getCuratedLocationProvider); - ref.invalidate(getCuratedPeopleProvider); + ref.invalidate(getAllPeopleProvider); } if (route.name == 'SharingRoute') { diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index 3ed8f69ea..55c105c74 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -72,21 +73,18 @@ class Album { @override bool operator ==(other) { if (other is! Album) return false; - - final lastModifiedAssetTimestampIsSetAndEqual = - lastModifiedAssetTimestamp != null && - other.lastModifiedAssetTimestamp != null - ? lastModifiedAssetTimestamp! - .isAtSameMomentAs(other.lastModifiedAssetTimestamp!) - : true; - return id == other.id && remoteId == other.remoteId && localId == other.localId && name == other.name && createdAt.isAtSameMomentAs(other.createdAt) && modifiedAt.isAtSameMomentAs(other.modifiedAt) && - lastModifiedAssetTimestampIsSetAndEqual && + isAtSameMomentAs(startDate, other.startDate) && + isAtSameMomentAs(endDate, other.endDate) && + isAtSameMomentAs( + lastModifiedAssetTimestamp, + other.lastModifiedAssetTimestamp, + ) && shared == other.shared && activityEnabled == other.activityEnabled && owner.value == other.owner.value && @@ -104,6 +102,8 @@ class Album { name.hashCode ^ createdAt.hashCode ^ modifiedAt.hashCode ^ + startDate.hashCode ^ + endDate.hashCode ^ lastModifiedAssetTimestamp.hashCode ^ shared.hashCode ^ activityEnabled.hashCode ^ diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index a441091d3..e547eb012 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -343,8 +344,13 @@ class SyncService { album.name = dto.albumName; album.shared = dto.shared; + album.createdAt = dto.createdAt; album.modifiedAt = dto.updatedAt; + album.startDate = dto.startDate; + album.endDate = dto.endDate; album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; + album.shared = dto.shared; + album.activityEnabled = dto.isActivityEnabled; if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) { album.thumbnail.value = await _db.assets .where() @@ -863,12 +869,10 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) { dto.shared != a.shared || dto.sharedUsers.length != a.sharedUsers.length || !dto.updatedAt.isAtSameMomentAs(a.modifiedAt) || - (dto.lastModifiedAssetTimestamp == null && - a.lastModifiedAssetTimestamp != null) || - (dto.lastModifiedAssetTimestamp != null && - a.lastModifiedAssetTimestamp == null) || - (dto.lastModifiedAssetTimestamp != null && - a.lastModifiedAssetTimestamp != null && - !dto.lastModifiedAssetTimestamp! - .isAtSameMomentAs(a.lastModifiedAssetTimestamp!)); + !isAtSameMomentAs(dto.startDate, a.startDate) || + !isAtSameMomentAs(dto.endDate, a.endDate) || + !isAtSameMomentAs( + dto.lastModifiedAssetTimestamp, + a.lastModifiedAssetTimestamp, + ); } diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart index 495f8f2f9..c9cc6c04a 100644 --- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart +++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart @@ -43,6 +43,7 @@ class MultiselectGrid extends HookConsumerWidget { this.editEnabled = false, this.unarchive = false, this.unfavorite = false, + this.emptyIndicator, }); final ProviderListenable> renderListProvider; @@ -57,12 +58,12 @@ class MultiselectGrid extends HookConsumerWidget { final bool favoriteEnabled; final bool unfavorite; final bool editEnabled; - + final Widget? emptyIndicator; Widget buildDefaultLoadingIndicator() => const Center(child: ImmichLoadingIndicator()); Widget buildEmptyIndicator() => - const Center(child: Text("No assets to show")); + emptyIndicator ?? const Center(child: Text("No assets to show")); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/shared/ui/immich_app_bar.dart b/mobile/lib/shared/ui/immich_app_bar.dart index 5b26432d8..678302dd9 100644 --- a/mobile/lib/shared/ui/immich_app_bar.dart +++ b/mobile/lib/shared/ui/immich_app_bar.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; @@ -169,11 +170,11 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { } return Padding( padding: const EdgeInsets.only(top: 3.0), - child: Image.asset( - height: 30, + child: SvgPicture.asset( context.isDarkTheme - ? 'assets/immich-logo-inline-dark.png' - : 'assets/immich-logo-inline-light.png', + ? 'assets/immich-logo-inline-dark.svg' + : 'assets/immich-logo-inline-light.svg', + height: 40, ), ); }, diff --git a/mobile/lib/shared/ui/immich_logo.dart b/mobile/lib/shared/ui/immich_logo.dart index af83887fb..9f7725aa1 100644 --- a/mobile/lib/shared/ui/immich_logo.dart +++ b/mobile/lib/shared/ui/immich_logo.dart @@ -18,6 +18,7 @@ class ImmichLogo extends StatelessWidget { image: const AssetImage('assets/immich-logo.png'), width: size, filterQuality: FilterQuality.high, + isAntiAlias: true, ), ); } diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 40850bdb4..40de493d0 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; -import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/tab.provider.dart'; @@ -53,10 +52,6 @@ class TabControllerPage extends HookConsumerWidget { // Scroll to top scrollToTopNotifierProvider.scrollToTop(); } - if (tabsRouter.activeIndex == 1 && index == 1) { - // Focus search - searchFocusNotifier.requestFocus(); - } HapticFeedback.selectionClick(); tabsRouter.setActiveIndex(index); @@ -111,10 +106,7 @@ class TabControllerPage extends HookConsumerWidget { // Scroll to top scrollToTopNotifierProvider.scrollToTop(); } - if (tabsRouter.activeIndex == 1 && index == 1) { - // Focus search - searchFocusNotifier.requestFocus(); - } + HapticFeedback.selectionClick(); tabsRouter.setActiveIndex(index); ref.read(tabProvider.notifier).state = TabEnum.values[index]; @@ -170,11 +162,11 @@ class TabControllerPage extends HookConsumerWidget { final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( - routes: [ - const HomeRoute(), + routes: const [ + HomeRoute(), SearchRoute(), - const SharingRoute(), - const LibraryRoute(), + SharingRoute(), + LibraryRoute(), ], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition( diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart new file mode 100644 index 000000000..8c53ea45b --- /dev/null +++ b/mobile/lib/utils/datetime_comparison.dart @@ -0,0 +1,3 @@ +bool isAtSameMomentAs(DateTime? a, DateTime? b) => + (a == null && b == null) || + ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index e2ed6cd56..07fac00e4 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -33,6 +33,9 @@ final ThemeData base = ThemeData( final ThemeData immichLightTheme = ThemeData( useMaterial3: true, brightness: Brightness.light, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigo, + ), primarySwatch: Colors.indigo, primaryColor: Colors.indigo, hintColor: Colors.indigo, @@ -158,6 +161,10 @@ final ThemeData immichDarkTheme = ThemeData( brightness: Brightness.dark, primarySwatch: Colors.indigo, primaryColor: immichDarkThemePrimaryColor, + colorScheme: ColorScheme.fromSeed( + seedColor: immichDarkThemePrimaryColor, + brightness: Brightness.dark, + ), scaffoldBackgroundColor: immichDarkBackgroundColor, hintColor: Colors.grey[600], fontFamily: 'Overpass', diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index ddebdaf77..4e109c14d 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -72,6 +72,7 @@ doc/FileChecksumResponseDto.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md +doc/ImageFormat.md doc/JobApi.md doc/JobCommand.md doc/JobCommandDto.md @@ -89,7 +90,12 @@ doc/LoginResponseDto.md doc/LogoutResponseDto.md doc/MapMarkerResponseDto.md doc/MapTheme.md +doc/MemoryApi.md +doc/MemoryCreateDto.md doc/MemoryLaneResponseDto.md +doc/MemoryResponseDto.md +doc/MemoryType.md +doc/MemoryUpdateDto.md doc/MergePersonDto.md doc/MetadataSearchDto.md doc/ModelType.md @@ -145,6 +151,7 @@ doc/SmartSearchDto.md doc/SystemConfigApi.md doc/SystemConfigDto.md doc/SystemConfigFFmpegDto.md +doc/SystemConfigImageDto.md doc/SystemConfigJobDto.md doc/SystemConfigLibraryDto.md doc/SystemConfigLibraryScanDto.md @@ -160,7 +167,6 @@ doc/SystemConfigServerDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md -doc/SystemConfigThumbnailDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md doc/TagApi.md @@ -169,6 +175,7 @@ doc/TagTypeEnum.md doc/ThumbnailFormat.md doc/TimeBucketResponseDto.md doc/TimeBucketSize.md +doc/TimelineApi.md doc/ToneMapping.md doc/TranscodeHWAccel.md doc/TranscodePolicy.md @@ -203,6 +210,7 @@ lib/api/download_api.dart lib/api/face_api.dart lib/api/job_api.dart lib/api/library_api.dart +lib/api/memory_api.dart lib/api/o_auth_api.dart lib/api/partner_api.dart lib/api/person_api.dart @@ -211,6 +219,7 @@ lib/api/server_info_api.dart lib/api/shared_link_api.dart lib/api/system_config_api.dart lib/api/tag_api.dart +lib/api/timeline_api.dart lib/api/trash_api.dart lib/api/user_api.dart lib/api_client.dart @@ -282,6 +291,7 @@ lib/model/file_checksum_response_dto.dart lib/model/file_report_dto.dart lib/model/file_report_fix_dto.dart lib/model/file_report_item_dto.dart +lib/model/image_format.dart lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart @@ -297,7 +307,11 @@ lib/model/login_response_dto.dart lib/model/logout_response_dto.dart lib/model/map_marker_response_dto.dart lib/model/map_theme.dart +lib/model/memory_create_dto.dart lib/model/memory_lane_response_dto.dart +lib/model/memory_response_dto.dart +lib/model/memory_type.dart +lib/model/memory_update_dto.dart lib/model/merge_person_dto.dart lib/model/metadata_search_dto.dart lib/model/model_type.dart @@ -346,6 +360,7 @@ lib/model/smart_info_response_dto.dart lib/model/smart_search_dto.dart lib/model/system_config_dto.dart lib/model/system_config_f_fmpeg_dto.dart +lib/model/system_config_image_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_library_dto.dart lib/model/system_config_library_scan_dto.dart @@ -361,7 +376,6 @@ lib/model/system_config_server_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_theme_dto.dart -lib/model/system_config_thumbnail_dto.dart lib/model/system_config_trash_dto.dart lib/model/system_config_user_dto.dart lib/model/tag_response_dto.dart @@ -459,6 +473,7 @@ test/file_checksum_response_dto_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart +test/image_format_test.dart test/job_api_test.dart test/job_command_dto_test.dart test/job_command_test.dart @@ -476,7 +491,12 @@ test/login_response_dto_test.dart test/logout_response_dto_test.dart test/map_marker_response_dto_test.dart test/map_theme_test.dart +test/memory_api_test.dart +test/memory_create_dto_test.dart test/memory_lane_response_dto_test.dart +test/memory_response_dto_test.dart +test/memory_type_test.dart +test/memory_update_dto_test.dart test/merge_person_dto_test.dart test/metadata_search_dto_test.dart test/model_type_test.dart @@ -532,6 +552,7 @@ test/smart_search_dto_test.dart test/system_config_api_test.dart test/system_config_dto_test.dart test/system_config_f_fmpeg_dto_test.dart +test/system_config_image_dto_test.dart test/system_config_job_dto_test.dart test/system_config_library_dto_test.dart test/system_config_library_scan_dto_test.dart @@ -547,7 +568,6 @@ test/system_config_server_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart -test/system_config_thumbnail_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart test/tag_api_test.dart @@ -556,6 +576,7 @@ test/tag_type_enum_test.dart test/thumbnail_format_test.dart test/time_bucket_response_dto_test.dart test/time_bucket_size_test.dart +test/timeline_api_test.dart test/tone_mapping_test.dart test/transcode_hw_accel_test.dart test/transcode_policy_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d8ff4d30f..fede2901c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.98.2 +- API version: 1.100.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -105,8 +105,6 @@ Class | Method | HTTP request | Description *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | *AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random | -*AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | -*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | *AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs | *AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets | *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -142,6 +140,13 @@ Class | Method | HTTP request | Description *LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan | *LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} | *LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /library/{id}/validate | +*MemoryApi* | [**addMemoryAssets**](doc//MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | +*MemoryApi* | [**createMemory**](doc//MemoryApi.md#creatememory) | **POST** /memories | +*MemoryApi* | [**deleteMemory**](doc//MemoryApi.md#deletememory) | **DELETE** /memories/{id} | +*MemoryApi* | [**getMemory**](doc//MemoryApi.md#getmemory) | **GET** /memories/{id} | +*MemoryApi* | [**removeMemoryAssets**](doc//MemoryApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | +*MemoryApi* | [**searchMemories**](doc//MemoryApi.md#searchmemories) | **GET** /memories | +*MemoryApi* | [**updateMemory**](doc//MemoryApi.md#updatememory) | **PUT** /memories/{id} | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | @@ -161,6 +166,7 @@ Class | Method | HTTP request | Description *PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign | *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | +*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | @@ -198,6 +204,8 @@ Class | Method | HTTP request | Description *TagApi* | [**tagAssets**](doc//TagApi.md#tagassets) | **PUT** /tag/{id}/assets | *TagApi* | [**untagAssets**](doc//TagApi.md#untagassets) | **DELETE** /tag/{id}/assets | *TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} | +*TimelineApi* | [**getTimeBucket**](doc//TimelineApi.md#gettimebucket) | **GET** /timeline/bucket | +*TimelineApi* | [**getTimeBuckets**](doc//TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets | *TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty | *TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets | *TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore | @@ -276,6 +284,7 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) + - [ImageFormat](doc//ImageFormat.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) @@ -291,7 +300,11 @@ Class | Method | HTTP request | Description - [LogoutResponseDto](doc//LogoutResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapTheme](doc//MapTheme.md) + - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) + - [MemoryResponseDto](doc//MemoryResponseDto.md) + - [MemoryType](doc//MemoryType.md) + - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) - [ModelType](doc//ModelType.md) @@ -340,6 +353,7 @@ Class | Method | HTTP request | Description - [SmartSearchDto](doc//SmartSearchDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md) @@ -355,7 +369,6 @@ Class | Method | HTTP request | Description - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) - [TagResponseDto](doc//TagResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 1aaf195f3..297a4cdba 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -23,8 +23,6 @@ Method | HTTP request | Description [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | [**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random | -[**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | -[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | [**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs | [**searchAssets**](AssetApi.md#searchassets) | **GET** /assets | [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | @@ -833,158 +831,6 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **getTimeBucket** -> List getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final size = ; // TimeBucketSize | -final timeBucket = timeBucket_example; // String | -final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final isArchived = true; // bool | -final isFavorite = true; // bool | -final isTrashed = true; // bool | -final key = key_example; // String | -final order = ; // AssetOrder | -final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final withPartners = true; // bool | -final withStacked = true; // bool | - -try { - final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getTimeBucket: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **size** | [**TimeBucketSize**](.md)| | - **timeBucket** | **String**| | - **albumId** | **String**| | [optional] - **isArchived** | **bool**| | [optional] - **isFavorite** | **bool**| | [optional] - **isTrashed** | **bool**| | [optional] - **key** | **String**| | [optional] - **order** | [**AssetOrder**](.md)| | [optional] - **personId** | **String**| | [optional] - **userId** | **String**| | [optional] - **withPartners** | **bool**| | [optional] - **withStacked** | **bool**| | [optional] - -### Return type - -[**List**](AssetResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **getTimeBuckets** -> List getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final size = ; // TimeBucketSize | -final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final isArchived = true; // bool | -final isFavorite = true; // bool | -final isTrashed = true; // bool | -final key = key_example; // String | -final order = ; // AssetOrder | -final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final withPartners = true; // bool | -final withStacked = true; // bool | - -try { - final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getTimeBuckets: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **size** | [**TimeBucketSize**](.md)| | - **albumId** | **String**| | [optional] - **isArchived** | **bool**| | [optional] - **isFavorite** | **bool**| | [optional] - **isTrashed** | **bool**| | [optional] - **key** | **String**| | [optional] - **order** | [**AssetOrder**](.md)| | [optional] - **personId** | **String**| | [optional] - **userId** | **String**| | [optional] - **withPartners** | **bool**| | [optional] - **withStacked** | **bool**| | [optional] - -### Return type - -[**List**](TimeBucketResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **runAssetJobs** > runAssetJobs(assetJobsDto) @@ -1040,7 +886,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **searchAssets** -> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) +> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) @@ -1090,11 +936,13 @@ final originalFileName = originalFileName_example; // String | final originalPath = originalPath_example; // String | final page = 8.14; // num | final personIds = []; // List | +final previewPath = previewPath_example; // String | final resizePath = resizePath_example; // String | final size = 8.14; // num | final state = state_example; // String | final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final thumbnailPath = thumbnailPath_example; // String | final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final type = ; // AssetTypeEnum | @@ -1108,7 +956,7 @@ final withPeople = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); + final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->searchAssets: $e\n'); @@ -1146,11 +994,13 @@ Name | Type | Description | Notes **originalPath** | **String**| | [optional] **page** | **num**| | [optional] **personIds** | [**List**](String.md)| | [optional] [default to const []] + **previewPath** | **String**| | [optional] **resizePath** | **String**| | [optional] **size** | **num**| | [optional] **state** | **String**| | [optional] **takenAfter** | **DateTime**| | [optional] **takenBefore** | **DateTime**| | [optional] + **thumbnailPath** | **String**| | [optional] **trashedAfter** | **DateTime**| | [optional] **trashedBefore** | **DateTime**| | [optional] **type** | [**AssetTypeEnum**](.md)| | [optional] diff --git a/mobile/openapi/doc/ImageFormat.md b/mobile/openapi/doc/ImageFormat.md new file mode 100644 index 000000000..312e501c1 --- /dev/null +++ b/mobile/openapi/doc/ImageFormat.md @@ -0,0 +1,14 @@ +# openapi.model.ImageFormat + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryApi.md b/mobile/openapi/doc/MemoryApi.md new file mode 100644 index 000000000..5795669a5 --- /dev/null +++ b/mobile/openapi/doc/MemoryApi.md @@ -0,0 +1,406 @@ +# openapi.api.MemoryApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**addMemoryAssets**](MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | +[**createMemory**](MemoryApi.md#creatememory) | **POST** /memories | +[**deleteMemory**](MemoryApi.md#deletememory) | **DELETE** /memories/{id} | +[**getMemory**](MemoryApi.md#getmemory) | **GET** /memories/{id} | +[**removeMemoryAssets**](MemoryApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | +[**searchMemories**](MemoryApi.md#searchmemories) | **GET** /memories | +[**updateMemory**](MemoryApi.md#updatememory) | **PUT** /memories/{id} | + + +# **addMemoryAssets** +> List addMemoryAssets(id, bulkIdsDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | + +try { + final result = api_instance.addMemoryAssets(id, bulkIdsDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->addMemoryAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | + +### Return type + +[**List**](BulkIdResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **createMemory** +> MemoryResponseDto createMemory(memoryCreateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final memoryCreateDto = MemoryCreateDto(); // MemoryCreateDto | + +try { + final result = api_instance.createMemory(memoryCreateDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->createMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **memoryCreateDto** | [**MemoryCreateDto**](MemoryCreateDto.md)| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **deleteMemory** +> deleteMemory(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + api_instance.deleteMemory(id); +} catch (e) { + print('Exception when calling MemoryApi->deleteMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getMemory** +> MemoryResponseDto getMemory(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + final result = api_instance.getMemory(id); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->getMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **removeMemoryAssets** +> List removeMemoryAssets(id, bulkIdsDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | + +try { + final result = api_instance.removeMemoryAssets(id, bulkIdsDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->removeMemoryAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | + +### Return type + +[**List**](BulkIdResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **searchMemories** +> List searchMemories() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); + +try { + final result = api_instance.searchMemories(); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->searchMemories: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**List**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **updateMemory** +> MemoryResponseDto updateMemory(id, memoryUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final memoryUpdateDto = MemoryUpdateDto(); // MemoryUpdateDto | + +try { + final result = api_instance.updateMemory(id, memoryUpdateDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->updateMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **memoryUpdateDto** | [**MemoryUpdateDto**](MemoryUpdateDto.md)| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/MemoryCreateDto.md b/mobile/openapi/doc/MemoryCreateDto.md new file mode 100644 index 000000000..5bcbd54f4 --- /dev/null +++ b/mobile/openapi/doc/MemoryCreateDto.md @@ -0,0 +1,20 @@ +# openapi.model.MemoryCreateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assetIds** | **List** | | [optional] [default to const []] +**data** | [**Object**](.md) | | +**isSaved** | **bool** | | [optional] +**memoryAt** | [**DateTime**](DateTime.md) | | +**seenAt** | [**DateTime**](DateTime.md) | | [optional] +**type** | [**MemoryType**](MemoryType.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryLaneResponseDto.md b/mobile/openapi/doc/MemoryLaneResponseDto.md index 471274446..54d1a4769 100644 --- a/mobile/openapi/doc/MemoryLaneResponseDto.md +++ b/mobile/openapi/doc/MemoryLaneResponseDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **assets** | [**List**](AssetResponseDto.md) | | [default to const []] **title** | **String** | | +**yearsAgo** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/MemoryResponseDto.md b/mobile/openapi/doc/MemoryResponseDto.md new file mode 100644 index 000000000..ef379be04 --- /dev/null +++ b/mobile/openapi/doc/MemoryResponseDto.md @@ -0,0 +1,25 @@ +# openapi.model.MemoryResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assets** | [**List**](AssetResponseDto.md) | | [default to const []] +**createdAt** | [**DateTime**](DateTime.md) | | +**data** | [**Object**](.md) | | +**deletedAt** | [**DateTime**](DateTime.md) | | [optional] +**id** | **String** | | +**isSaved** | **bool** | | +**memoryAt** | [**DateTime**](DateTime.md) | | +**ownerId** | **String** | | +**seenAt** | [**DateTime**](DateTime.md) | | [optional] +**type** | **String** | | +**updatedAt** | [**DateTime**](DateTime.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryType.md b/mobile/openapi/doc/MemoryType.md new file mode 100644 index 000000000..c8dea25be --- /dev/null +++ b/mobile/openapi/doc/MemoryType.md @@ -0,0 +1,14 @@ +# openapi.model.MemoryType + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryUpdateDto.md b/mobile/openapi/doc/MemoryUpdateDto.md new file mode 100644 index 000000000..7a48e84e8 --- /dev/null +++ b/mobile/openapi/doc/MemoryUpdateDto.md @@ -0,0 +1,17 @@ +# openapi.model.MemoryUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isSaved** | **bool** | | [optional] +**memoryAt** | [**DateTime**](DateTime.md) | | [optional] +**seenAt** | [**DateTime**](DateTime.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MetadataSearchDto.md b/mobile/openapi/doc/MetadataSearchDto.md index d1d098fb0..5dc50c00f 100644 --- a/mobile/openapi/doc/MetadataSearchDto.md +++ b/mobile/openapi/doc/MetadataSearchDto.md @@ -35,11 +35,13 @@ Name | Type | Description | Notes **originalPath** | **String** | | [optional] **page** | **num** | | [optional] **personIds** | **List** | | [optional] [default to const []] +**previewPath** | **String** | | [optional] **resizePath** | **String** | | [optional] **size** | **num** | | [optional] **state** | **String** | | [optional] **takenAfter** | [**DateTime**](DateTime.md) | | [optional] **takenBefore** | [**DateTime**](DateTime.md) | | [optional] +**thumbnailPath** | **String** | | [optional] **trashedAfter** | [**DateTime**](DateTime.md) | | [optional] **trashedBefore** | [**DateTime**](DateTime.md) | | [optional] **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional] diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index f63488222..e4ab9ecfd 100644 --- a/mobile/openapi/doc/SearchApi.md +++ b/mobile/openapi/doc/SearchApi.md @@ -9,6 +9,7 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- +[**getAssetsByCity**](SearchApi.md#getassetsbycity) | **GET** /search/cities | [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**getSearchSuggestions**](SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | [**search**](SearchApi.md#search) | **GET** /search | @@ -18,6 +19,57 @@ Method | HTTP request | Description [**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart | +# **getAssetsByCity** +> List getAssetsByCity() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); + +try { + final result = api_instance.getAssetsByCity(); + print(result); +} catch (e) { + print('Exception when calling SearchApi->getAssetsByCity: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**List**](AssetResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getExploreData** > List getExploreData() diff --git a/mobile/openapi/doc/SystemConfigApi.md b/mobile/openapi/doc/SystemConfigApi.md index e782265b4..2d1ce0da6 100644 --- a/mobile/openapi/doc/SystemConfigApi.md +++ b/mobile/openapi/doc/SystemConfigApi.md @@ -119,7 +119,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMapStyle** -> Object getMapStyle(theme) +> Object getMapStyle(theme, key) @@ -143,9 +143,10 @@ import 'package:openapi/api.dart'; final api_instance = SystemConfigApi(); final theme = ; // MapTheme | +final key = key_example; // String | try { - final result = api_instance.getMapStyle(theme); + final result = api_instance.getMapStyle(theme, key); print(result); } catch (e) { print('Exception when calling SystemConfigApi->getMapStyle: $e\n'); @@ -157,6 +158,7 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **theme** | [**MapTheme**](.md)| | + **key** | **String**| | [optional] ### Return type diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index ad1afbe9f..fc3d3dd0b 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | +**image** | [**SystemConfigImageDto**](SystemConfigImageDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | | **logging** | [**SystemConfigLoggingDto**](SystemConfigLoggingDto.md) | | @@ -21,7 +22,6 @@ Name | Type | Description | Notes **server** | [**SystemConfigServerDto**](SystemConfigServerDto.md) | | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | | **theme** | [**SystemConfigThemeDto**](SystemConfigThemeDto.md) | | -**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | | **trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | | **user** | [**SystemConfigUserDto**](SystemConfigUserDto.md) | | diff --git a/mobile/openapi/doc/SystemConfigThumbnailDto.md b/mobile/openapi/doc/SystemConfigImageDto.md similarity index 65% rename from mobile/openapi/doc/SystemConfigThumbnailDto.md rename to mobile/openapi/doc/SystemConfigImageDto.md index 491bf9f12..1b9bbe726 100644 --- a/mobile/openapi/doc/SystemConfigThumbnailDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigThumbnailDto +# openapi.model.SystemConfigImageDto ## Load the model package ```dart @@ -9,9 +9,11 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | -**jpegSize** | **int** | | +**previewFormat** | [**ImageFormat**](ImageFormat.md) | | +**previewSize** | **int** | | **quality** | **int** | | -**webpSize** | **int** | | +**thumbnailFormat** | [**ImageFormat**](ImageFormat.md) | | +**thumbnailSize** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/TimelineApi.md b/mobile/openapi/doc/TimelineApi.md new file mode 100644 index 000000000..e98efe7e2 --- /dev/null +++ b/mobile/openapi/doc/TimelineApi.md @@ -0,0 +1,167 @@ +# openapi.api.TimelineApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getTimeBucket**](TimelineApi.md#gettimebucket) | **GET** /timeline/bucket | +[**getTimeBuckets**](TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets | + + +# **getTimeBucket** +> List getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = TimelineApi(); +final size = ; // TimeBucketSize | +final timeBucket = timeBucket_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final isArchived = true; // bool | +final isFavorite = true; // bool | +final isTrashed = true; // bool | +final key = key_example; // String | +final order = ; // AssetOrder | +final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final withPartners = true; // bool | +final withStacked = true; // bool | + +try { + final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); + print(result); +} catch (e) { + print('Exception when calling TimelineApi->getTimeBucket: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **size** | [**TimeBucketSize**](.md)| | + **timeBucket** | **String**| | + **albumId** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isTrashed** | **bool**| | [optional] + **key** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] + **personId** | **String**| | [optional] + **userId** | **String**| | [optional] + **withPartners** | **bool**| | [optional] + **withStacked** | **bool**| | [optional] + +### Return type + +[**List**](AssetResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getTimeBuckets** +> List getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = TimelineApi(); +final size = ; // TimeBucketSize | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final isArchived = true; // bool | +final isFavorite = true; // bool | +final isTrashed = true; // bool | +final key = key_example; // String | +final order = ; // AssetOrder | +final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final withPartners = true; // bool | +final withStacked = true; // bool | + +try { + final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); + print(result); +} catch (e) { + print('Exception when calling TimelineApi->getTimeBuckets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **size** | [**TimeBucketSize**](.md)| | + **albumId** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isTrashed** | **bool**| | [optional] + **key** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] + **personId** | **String**| | [optional] + **userId** | **String**| | [optional] + **withPartners** | **bool**| | [optional] + **withStacked** | **bool**| | [optional] + +### Return type + +[**List**](TimeBucketResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 5b49d8d67..7d8ab5288 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -39,6 +39,7 @@ part 'api/download_api.dart'; part 'api/face_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; +part 'api/memory_api.dart'; part 'api/o_auth_api.dart'; part 'api/partner_api.dart'; part 'api/person_api.dart'; @@ -47,6 +48,7 @@ part 'api/server_info_api.dart'; part 'api/shared_link_api.dart'; part 'api/system_config_api.dart'; part 'api/tag_api.dart'; +part 'api/timeline_api.dart'; part 'api/trash_api.dart'; part 'api/user_api.dart'; @@ -111,6 +113,7 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; +part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; @@ -126,7 +129,11 @@ part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_theme.dart'; +part 'model/memory_create_dto.dart'; part 'model/memory_lane_response_dto.dart'; +part 'model/memory_response_dto.dart'; +part 'model/memory_type.dart'; +part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; part 'model/model_type.dart'; @@ -175,6 +182,7 @@ part 'model/smart_info_response_dto.dart'; part 'model/smart_search_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; part 'model/system_config_library_scan_dto.dart'; @@ -190,7 +198,6 @@ part 'model/system_config_server_dto.dart'; part 'model/system_config_storage_template_dto.dart'; part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; -part 'model/system_config_thumbnail_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; part 'model/tag_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index b0395bfcb..10f1c0c10 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -835,255 +835,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'GET /asset/time-bucket' operation and returns the [Response]. - /// Parameters: - /// - /// * [TimeBucketSize] size (required): - /// - /// * [String] timeBucket (required): - /// - /// * [String] albumId: - /// - /// * [bool] isArchived: - /// - /// * [bool] isFavorite: - /// - /// * [bool] isTrashed: - /// - /// * [String] key: - /// - /// * [AssetOrder] order: - /// - /// * [String] personId: - /// - /// * [String] userId: - /// - /// * [bool] withPartners: - /// - /// * [bool] withStacked: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/time-bucket'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (albumId != null) { - queryParams.addAll(_queryParams('', 'albumId', albumId)); - } - if (isArchived != null) { - queryParams.addAll(_queryParams('', 'isArchived', isArchived)); - } - if (isFavorite != null) { - queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); - } - if (isTrashed != null) { - queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); - } - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (order != null) { - queryParams.addAll(_queryParams('', 'order', order)); - } - if (personId != null) { - queryParams.addAll(_queryParams('', 'personId', personId)); - } - queryParams.addAll(_queryParams('', 'size', size)); - queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); - if (userId != null) { - queryParams.addAll(_queryParams('', 'userId', userId)); - } - if (withPartners != null) { - queryParams.addAll(_queryParams('', 'withPartners', withPartners)); - } - if (withStacked != null) { - queryParams.addAll(_queryParams('', 'withStacked', withStacked)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [TimeBucketSize] size (required): - /// - /// * [String] timeBucket (required): - /// - /// * [String] albumId: - /// - /// * [bool] isArchived: - /// - /// * [bool] isFavorite: - /// - /// * [bool] isTrashed: - /// - /// * [String] key: - /// - /// * [AssetOrder] order: - /// - /// * [String] personId: - /// - /// * [String] userId: - /// - /// * [bool] withPartners: - /// - /// * [bool] withStacked: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Performs an HTTP 'GET /asset/time-buckets' operation and returns the [Response]. - /// Parameters: - /// - /// * [TimeBucketSize] size (required): - /// - /// * [String] albumId: - /// - /// * [bool] isArchived: - /// - /// * [bool] isFavorite: - /// - /// * [bool] isTrashed: - /// - /// * [String] key: - /// - /// * [AssetOrder] order: - /// - /// * [String] personId: - /// - /// * [String] userId: - /// - /// * [bool] withPartners: - /// - /// * [bool] withStacked: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - // ignore: prefer_const_declarations - final path = r'/asset/time-buckets'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (albumId != null) { - queryParams.addAll(_queryParams('', 'albumId', albumId)); - } - if (isArchived != null) { - queryParams.addAll(_queryParams('', 'isArchived', isArchived)); - } - if (isFavorite != null) { - queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); - } - if (isTrashed != null) { - queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); - } - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (order != null) { - queryParams.addAll(_queryParams('', 'order', order)); - } - if (personId != null) { - queryParams.addAll(_queryParams('', 'personId', personId)); - } - queryParams.addAll(_queryParams('', 'size', size)); - if (userId != null) { - queryParams.addAll(_queryParams('', 'userId', userId)); - } - if (withPartners != null) { - queryParams.addAll(_queryParams('', 'withPartners', withPartners)); - } - if (withStacked != null) { - queryParams.addAll(_queryParams('', 'withStacked', withStacked)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [TimeBucketSize] size (required): - /// - /// * [String] albumId: - /// - /// * [bool] isArchived: - /// - /// * [bool] isFavorite: - /// - /// * [bool] isTrashed: - /// - /// * [String] key: - /// - /// * [AssetOrder] order: - /// - /// * [String] personId: - /// - /// * [String] userId: - /// - /// * [bool] withPartners: - /// - /// * [bool] withStacked: - Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response]. /// Parameters: /// @@ -1180,6 +931,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -1190,6 +943,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -1211,7 +966,7 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -1303,6 +1058,9 @@ class AssetApi { if (personIds != null) { queryParams.addAll(_queryParams('multi', 'personIds', personIds)); } + if (previewPath != null) { + queryParams.addAll(_queryParams('', 'previewPath', previewPath)); + } if (resizePath != null) { queryParams.addAll(_queryParams('', 'resizePath', resizePath)); } @@ -1318,6 +1076,9 @@ class AssetApi { if (takenBefore != null) { queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); } + if (thumbnailPath != null) { + queryParams.addAll(_queryParams('', 'thumbnailPath', thumbnailPath)); + } if (trashedAfter != null) { queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); } @@ -1422,6 +1183,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -1432,6 +1195,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -1453,8 +1218,8 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { - final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); + Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, previewPath: previewPath, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, thumbnailPath: thumbnailPath, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/memory_api.dart b/mobile/openapi/lib/api/memory_api.dart new file mode 100644 index 000000000..6b4a619b5 --- /dev/null +++ b/mobile/openapi/lib/api/memory_api.dart @@ -0,0 +1,359 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class MemoryApi { + MemoryApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'PUT /memories/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'POST /memories' operation and returns the [Response]. + /// Parameters: + /// + /// * [MemoryCreateDto] memoryCreateDto (required): + Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories'; + + // ignore: prefer_final_locals + Object? postBody = memoryCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [MemoryCreateDto] memoryCreateDto (required): + Future createMemory(MemoryCreateDto memoryCreateDto,) async { + final response = await createMemoryWithHttpInfo(memoryCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteMemoryWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteMemory(String id,) async { + final response = await deleteMemoryWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getMemoryWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getMemory(String id,) async { + final response = await getMemoryWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /memories/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'GET /memories' operation and returns the [Response]. + Future searchMemoriesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/memories'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> searchMemories() async { + final response = await searchMemoriesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'PUT /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MemoryUpdateDto] memoryUpdateDto (required): + Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = memoryUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MemoryUpdateDto] memoryUpdateDto (required): + Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto,) async { + final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 3a0bc56bb..386a2f353 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -16,6 +16,50 @@ class SearchApi { final ApiClient apiClient; + /// Performs an HTTP 'GET /search/cities' operation and returns the [Response]. + Future getAssetsByCityWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/search/cities'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> getAssetsByCity() async { + final response = await getAssetsByCityWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Performs an HTTP 'GET /search/explore' operation and returns the [Response]. Future getExploreDataWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index f13f8d52d..276f8c07d 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -102,7 +102,9 @@ class SystemConfigApi { /// Parameters: /// /// * [MapTheme] theme (required): - Future getMapStyleWithHttpInfo(MapTheme theme,) async { + /// + /// * [String] key: + Future getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async { // ignore: prefer_const_declarations final path = r'/system-config/map/style.json'; @@ -113,6 +115,9 @@ class SystemConfigApi { final headerParams = {}; final formParams = {}; + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } queryParams.addAll(_queryParams('', 'theme', theme)); const contentTypes = []; @@ -132,8 +137,10 @@ class SystemConfigApi { /// Parameters: /// /// * [MapTheme] theme (required): - Future getMapStyle(MapTheme theme,) async { - final response = await getMapStyleWithHttpInfo(theme,); + /// + /// * [String] key: + Future getMapStyle(MapTheme theme, { String? key, }) async { + final response = await getMapStyleWithHttpInfo(theme, key: key, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart new file mode 100644 index 000000000..0813f3e00 --- /dev/null +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -0,0 +1,267 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class TimelineApi { + TimelineApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response]. + /// Parameters: + /// + /// * [TimeBucketSize] size (required): + /// + /// * [String] timeBucket (required): + /// + /// * [String] albumId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [String] key: + /// + /// * [AssetOrder] order: + /// + /// * [String] personId: + /// + /// * [String] userId: + /// + /// * [bool] withPartners: + /// + /// * [bool] withStacked: + Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + // ignore: prefer_const_declarations + final path = r'/timeline/bucket'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (albumId != null) { + queryParams.addAll(_queryParams('', 'albumId', albumId)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isTrashed != null) { + queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); + } + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (personId != null) { + queryParams.addAll(_queryParams('', 'personId', personId)); + } + queryParams.addAll(_queryParams('', 'size', size)); + queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); + if (userId != null) { + queryParams.addAll(_queryParams('', 'userId', userId)); + } + if (withPartners != null) { + queryParams.addAll(_queryParams('', 'withPartners', withPartners)); + } + if (withStacked != null) { + queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TimeBucketSize] size (required): + /// + /// * [String] timeBucket (required): + /// + /// * [String] albumId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [String] key: + /// + /// * [AssetOrder] order: + /// + /// * [String] personId: + /// + /// * [String] userId: + /// + /// * [bool] withPartners: + /// + /// * [bool] withStacked: + Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response]. + /// Parameters: + /// + /// * [TimeBucketSize] size (required): + /// + /// * [String] albumId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [String] key: + /// + /// * [AssetOrder] order: + /// + /// * [String] personId: + /// + /// * [String] userId: + /// + /// * [bool] withPartners: + /// + /// * [bool] withStacked: + Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + // ignore: prefer_const_declarations + final path = r'/timeline/buckets'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (albumId != null) { + queryParams.addAll(_queryParams('', 'albumId', albumId)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isTrashed != null) { + queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); + } + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (personId != null) { + queryParams.addAll(_queryParams('', 'personId', personId)); + } + queryParams.addAll(_queryParams('', 'size', size)); + if (userId != null) { + queryParams.addAll(_queryParams('', 'userId', userId)); + } + if (withPartners != null) { + queryParams.addAll(_queryParams('', 'withPartners', withPartners)); + } + if (withStacked != null) { + queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TimeBucketSize] size (required): + /// + /// * [String] albumId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [String] key: + /// + /// * [AssetOrder] order: + /// + /// * [String] personId: + /// + /// * [String] userId: + /// + /// * [bool] withPartners: + /// + /// * [bool] withStacked: + Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 312153788..8784ad641 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -304,6 +304,8 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); + case 'ImageFormat': + return ImageFormatTypeTransformer().decode(value); case 'JobCommand': return JobCommandTypeTransformer().decode(value); case 'JobCommandDto': @@ -334,8 +336,16 @@ class ApiClient { return MapMarkerResponseDto.fromJson(value); case 'MapTheme': return MapThemeTypeTransformer().decode(value); + case 'MemoryCreateDto': + return MemoryCreateDto.fromJson(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); + case 'MemoryResponseDto': + return MemoryResponseDto.fromJson(value); + case 'MemoryType': + return MemoryTypeTypeTransformer().decode(value); + case 'MemoryUpdateDto': + return MemoryUpdateDto.fromJson(value); case 'MergePersonDto': return MergePersonDto.fromJson(value); case 'MetadataSearchDto': @@ -432,6 +442,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigImageDto': + return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': return SystemConfigJobDto.fromJson(value); case 'SystemConfigLibraryDto': @@ -462,8 +474,6 @@ class ApiClient { return SystemConfigTemplateStorageOptionDto.fromJson(value); case 'SystemConfigThemeDto': return SystemConfigThemeDto.fromJson(value); - case 'SystemConfigThumbnailDto': - return SystemConfigThumbnailDto.fromJson(value); case 'SystemConfigTrashDto': return SystemConfigTrashDto.fromJson(value); case 'SystemConfigUserDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index d186845d9..7ad74d951 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -79,6 +79,9 @@ String parameterToString(dynamic value) { if (value is EntityType) { return EntityTypeTypeTransformer().encode(value).toString(); } + if (value is ImageFormat) { + return ImageFormatTypeTransformer().encode(value).toString(); + } if (value is JobCommand) { return JobCommandTypeTransformer().encode(value).toString(); } @@ -94,6 +97,9 @@ String parameterToString(dynamic value) { if (value is MapTheme) { return MapThemeTypeTransformer().encode(value).toString(); } + if (value is MemoryType) { + return MemoryTypeTypeTransformer().encode(value).toString(); + } if (value is ModelType) { return ModelTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/image_format.dart b/mobile/openapi/lib/model/image_format.dart new file mode 100644 index 000000000..570b6ca6e --- /dev/null +++ b/mobile/openapi/lib/model/image_format.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ImageFormat { + /// Instantiate a new enum with the provided [value]. + const ImageFormat._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const jpeg = ImageFormat._(r'jpeg'); + static const webp = ImageFormat._(r'webp'); + + /// List of all possible values in this [enum][ImageFormat]. + static const values = [ + jpeg, + webp, + ]; + + static ImageFormat? fromJson(dynamic value) => ImageFormatTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ImageFormat.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ImageFormat] to String, +/// and [decode] dynamic data back to [ImageFormat]. +class ImageFormatTypeTransformer { + factory ImageFormatTypeTransformer() => _instance ??= const ImageFormatTypeTransformer._(); + + const ImageFormatTypeTransformer._(); + + String encode(ImageFormat data) => data.value; + + /// Decodes a [dynamic value][data] to a ImageFormat. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ImageFormat? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'jpeg': return ImageFormat.jpeg; + case r'webp': return ImageFormat.webp; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ImageFormatTypeTransformer] instance. + static ImageFormatTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart new file mode 100644 index 000000000..5d08a631c --- /dev/null +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -0,0 +1,157 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryCreateDto { + /// Returns a new [MemoryCreateDto] instance. + MemoryCreateDto({ + this.assetIds = const [], + required this.data, + this.isSaved, + required this.memoryAt, + this.seenAt, + required this.type, + }); + + List assetIds; + + Object data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isSaved; + + DateTime memoryAt; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + MemoryType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && + _deepEquality.equals(other.assetIds, assetIds) && + other.data == data && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode) + + (data.hashCode) + + (isSaved == null ? 0 : isSaved!.hashCode) + + (memoryAt.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode) + + (type.hashCode); + + @override + String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, type=$type]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + json[r'data'] = this.data; + if (this.isSaved != null) { + json[r'isSaved'] = this.isSaved; + } else { + // json[r'isSaved'] = null; + } + json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + json[r'type'] = this.type; + return json; + } + + /// Returns a new [MemoryCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryCreateDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + data: mapValueOfType(json, r'data')!, + isSaved: mapValueOfType(json, r'isSaved'), + memoryAt: mapDateTime(json, r'memoryAt', r'')!, + seenAt: mapDateTime(json, r'seenAt', r''), + type: MemoryType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'data', + 'memoryAt', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/memory_lane_response_dto.dart b/mobile/openapi/lib/model/memory_lane_response_dto.dart index 7d761131d..a0df07938 100644 --- a/mobile/openapi/lib/model/memory_lane_response_dto.dart +++ b/mobile/openapi/lib/model/memory_lane_response_dto.dart @@ -15,30 +15,36 @@ class MemoryLaneResponseDto { MemoryLaneResponseDto({ this.assets = const [], required this.title, + required this.yearsAgo, }); List assets; String title; + int yearsAgo; + @override bool operator ==(Object other) => identical(this, other) || other is MemoryLaneResponseDto && _deepEquality.equals(other.assets, assets) && - other.title == title; + other.title == title && + other.yearsAgo == yearsAgo; @override int get hashCode => // ignore: unnecessary_parenthesis (assets.hashCode) + - (title.hashCode); + (title.hashCode) + + (yearsAgo.hashCode); @override - String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title]'; + String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title, yearsAgo=$yearsAgo]'; Map toJson() { final json = {}; json[r'assets'] = this.assets; json[r'title'] = this.title; + json[r'yearsAgo'] = this.yearsAgo; return json; } @@ -52,6 +58,7 @@ class MemoryLaneResponseDto { return MemoryLaneResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), title: mapValueOfType(json, r'title')!, + yearsAgo: mapValueOfType(json, r'yearsAgo')!, ); } return null; @@ -101,6 +108,7 @@ class MemoryLaneResponseDto { static const requiredKeys = { 'assets', 'title', + 'yearsAgo', }; } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart new file mode 100644 index 000000000..918099458 --- /dev/null +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -0,0 +1,267 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryResponseDto { + /// Returns a new [MemoryResponseDto] instance. + MemoryResponseDto({ + this.assets = const [], + required this.createdAt, + required this.data, + this.deletedAt, + required this.id, + required this.isSaved, + required this.memoryAt, + required this.ownerId, + this.seenAt, + required this.type, + required this.updatedAt, + }); + + List assets; + + DateTime createdAt; + + Object data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? deletedAt; + + String id; + + bool isSaved; + + DateTime memoryAt; + + String ownerId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + MemoryResponseDtoTypeEnum type; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryResponseDto && + _deepEquality.equals(other.assets, assets) && + other.createdAt == createdAt && + other.data == data && + other.deletedAt == deletedAt && + other.id == id && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.ownerId == ownerId && + other.seenAt == seenAt && + other.type == type && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assets.hashCode) + + (createdAt.hashCode) + + (data.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (id.hashCode) + + (isSaved.hashCode) + + (memoryAt.hashCode) + + (ownerId.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode) + + (type.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'MemoryResponseDto[assets=$assets, createdAt=$createdAt, data=$data, deletedAt=$deletedAt, id=$id, isSaved=$isSaved, memoryAt=$memoryAt, ownerId=$ownerId, seenAt=$seenAt, type=$type, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'assets'] = this.assets; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'data'] = this.data; + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'id'] = this.id; + json[r'isSaved'] = this.isSaved; + json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'ownerId'] = this.ownerId; + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + json[r'type'] = this.type; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [MemoryResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryResponseDto( + assets: AssetResponseDto.listFromJson(json[r'assets']), + createdAt: mapDateTime(json, r'createdAt', r'')!, + data: mapValueOfType(json, r'data')!, + deletedAt: mapDateTime(json, r'deletedAt', r''), + id: mapValueOfType(json, r'id')!, + isSaved: mapValueOfType(json, r'isSaved')!, + memoryAt: mapDateTime(json, r'memoryAt', r'')!, + ownerId: mapValueOfType(json, r'ownerId')!, + seenAt: mapDateTime(json, r'seenAt', r''), + type: MemoryResponseDtoTypeEnum.fromJson(json[r'type'])!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assets', + 'createdAt', + 'data', + 'id', + 'isSaved', + 'memoryAt', + 'ownerId', + 'type', + 'updatedAt', + }; +} + + +class MemoryResponseDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const MemoryResponseDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryResponseDtoTypeEnum._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryResponseDtoTypeEnum]. + static const values = [ + onThisDay, + ]; + + static MemoryResponseDtoTypeEnum? fromJson(dynamic value) => MemoryResponseDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponseDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryResponseDtoTypeEnum] to String, +/// and [decode] dynamic data back to [MemoryResponseDtoTypeEnum]. +class MemoryResponseDtoTypeEnumTypeTransformer { + factory MemoryResponseDtoTypeEnumTypeTransformer() => _instance ??= const MemoryResponseDtoTypeEnumTypeTransformer._(); + + const MemoryResponseDtoTypeEnumTypeTransformer._(); + + String encode(MemoryResponseDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryResponseDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryResponseDtoTypeEnum.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryResponseDtoTypeEnumTypeTransformer] instance. + static MemoryResponseDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart new file mode 100644 index 000000000..513b7c2d4 --- /dev/null +++ b/mobile/openapi/lib/model/memory_type.dart @@ -0,0 +1,82 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class MemoryType { + /// Instantiate a new enum with the provided [value]. + const MemoryType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryType._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryType]. + static const values = [ + onThisDay, + ]; + + static MemoryType? fromJson(dynamic value) => MemoryTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryType] to String, +/// and [decode] dynamic data back to [MemoryType]. +class MemoryTypeTypeTransformer { + factory MemoryTypeTypeTransformer() => _instance ??= const MemoryTypeTypeTransformer._(); + + const MemoryTypeTypeTransformer._(); + + String encode(MemoryType data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryType.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryTypeTypeTransformer] instance. + static MemoryTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart new file mode 100644 index 000000000..adf42330d --- /dev/null +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -0,0 +1,141 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryUpdateDto { + /// Returns a new [MemoryUpdateDto] instance. + MemoryUpdateDto({ + this.isSaved, + this.memoryAt, + this.seenAt, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isSaved; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? memoryAt; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryUpdateDto && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isSaved == null ? 0 : isSaved!.hashCode) + + (memoryAt == null ? 0 : memoryAt!.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode); + + @override + String toString() => 'MemoryUpdateDto[isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt]'; + + Map toJson() { + final json = {}; + if (this.isSaved != null) { + json[r'isSaved'] = this.isSaved; + } else { + // json[r'isSaved'] = null; + } + if (this.memoryAt != null) { + json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + } else { + // json[r'memoryAt'] = null; + } + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + return json; + } + + /// Returns a new [MemoryUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryUpdateDto( + isSaved: mapValueOfType(json, r'isSaved'), + memoryAt: mapDateTime(json, r'memoryAt', r''), + seenAt: mapDateTime(json, r'seenAt', r''), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 86a2856e6..3f770ed09 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -40,11 +40,13 @@ class MetadataSearchDto { this.originalPath, this.page, this.personIds = const [], + this.previewPath, this.resizePath, this.size, this.state, this.takenAfter, this.takenBefore, + this.thumbnailPath, this.trashedAfter, this.trashedBefore, this.type, @@ -268,6 +270,14 @@ class MetadataSearchDto { List personIds; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? previewPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -308,6 +318,14 @@ class MetadataSearchDto { /// DateTime? takenBefore; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? thumbnailPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -419,11 +437,13 @@ class MetadataSearchDto { other.originalPath == originalPath && other.page == page && _deepEquality.equals(other.personIds, personIds) && + other.previewPath == previewPath && other.resizePath == resizePath && other.size == size && other.state == state && other.takenAfter == takenAfter && other.takenBefore == takenBefore && + other.thumbnailPath == thumbnailPath && other.trashedAfter == trashedAfter && other.trashedBefore == trashedBefore && other.type == type && @@ -466,11 +486,13 @@ class MetadataSearchDto { (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + (personIds.hashCode) + + (previewPath == null ? 0 : previewPath!.hashCode) + (resizePath == null ? 0 : resizePath!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + (type == null ? 0 : type!.hashCode) + @@ -484,7 +506,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -619,6 +641,11 @@ class MetadataSearchDto { // json[r'page'] = null; } json[r'personIds'] = this.personIds; + if (this.previewPath != null) { + json[r'previewPath'] = this.previewPath; + } else { + // json[r'previewPath'] = null; + } if (this.resizePath != null) { json[r'resizePath'] = this.resizePath; } else { @@ -644,6 +671,11 @@ class MetadataSearchDto { } else { // json[r'takenBefore'] = null; } + if (this.thumbnailPath != null) { + json[r'thumbnailPath'] = this.thumbnailPath; + } else { + // json[r'thumbnailPath'] = null; + } if (this.trashedAfter != null) { json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); } else { @@ -735,11 +767,13 @@ class MetadataSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], + previewPath: mapValueOfType(json, r'previewPath'), resizePath: mapValueOfType(json, r'resizePath'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), takenAfter: mapDateTime(json, r'takenAfter', r''), takenBefore: mapDateTime(json, r'takenBefore', r''), + thumbnailPath: mapValueOfType(json, r'thumbnailPath'), trashedAfter: mapDateTime(json, r'trashedAfter', r''), trashedBefore: mapDateTime(json, r'trashedBefore', r''), type: AssetTypeEnum.fromJson(json[r'type']), diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart index ea722076d..11cdf41ea 100644 --- a/mobile/openapi/lib/model/path_type.dart +++ b/mobile/openapi/lib/model/path_type.dart @@ -24,8 +24,8 @@ class PathType { String toJson() => value; static const original = PathType._(r'original'); - static const jpegThumbnail = PathType._(r'jpeg_thumbnail'); - static const webpThumbnail = PathType._(r'webp_thumbnail'); + static const preview = PathType._(r'preview'); + static const thumbnail = PathType._(r'thumbnail'); static const encodedVideo = PathType._(r'encoded_video'); static const sidecar = PathType._(r'sidecar'); static const face = PathType._(r'face'); @@ -34,8 +34,8 @@ class PathType { /// List of all possible values in this [enum][PathType]. static const values = [ original, - jpegThumbnail, - webpThumbnail, + preview, + thumbnail, encodedVideo, sidecar, face, @@ -79,8 +79,8 @@ class PathTypeTypeTransformer { if (data != null) { switch (data) { case r'original': return PathType.original; - case r'jpeg_thumbnail': return PathType.jpegThumbnail; - case r'webp_thumbnail': return PathType.webpThumbnail; + case r'preview': return PathType.preview; + case r'thumbnail': return PathType.thumbnail; case r'encoded_video': return PathType.encodedVideo; case r'sidecar': return PathType.sidecar; case r'face': return PathType.face; diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 0b5f64fc2..f075d37c8 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -14,6 +14,7 @@ class SystemConfigDto { /// Returns a new [SystemConfigDto] instance. SystemConfigDto({ required this.ffmpeg, + required this.image, required this.job, required this.library_, required this.logging, @@ -26,13 +27,14 @@ class SystemConfigDto { required this.server, required this.storageTemplate, required this.theme, - required this.thumbnail, required this.trash, required this.user, }); SystemConfigFFmpegDto ffmpeg; + SystemConfigImageDto image; + SystemConfigJobDto job; SystemConfigLibraryDto library_; @@ -57,8 +59,6 @@ class SystemConfigDto { SystemConfigThemeDto theme; - SystemConfigThumbnailDto thumbnail; - SystemConfigTrashDto trash; SystemConfigUserDto user; @@ -66,6 +66,7 @@ class SystemConfigDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && other.ffmpeg == ffmpeg && + other.image == image && other.job == job && other.library_ == library_ && other.logging == logging && @@ -78,7 +79,6 @@ class SystemConfigDto { other.server == server && other.storageTemplate == storageTemplate && other.theme == theme && - other.thumbnail == thumbnail && other.trash == trash && other.user == user; @@ -86,6 +86,7 @@ class SystemConfigDto { int get hashCode => // ignore: unnecessary_parenthesis (ffmpeg.hashCode) + + (image.hashCode) + (job.hashCode) + (library_.hashCode) + (logging.hashCode) + @@ -98,16 +99,16 @@ class SystemConfigDto { (server.hashCode) + (storageTemplate.hashCode) + (theme.hashCode) + - (thumbnail.hashCode) + (trash.hashCode) + (user.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; json[r'ffmpeg'] = this.ffmpeg; + json[r'image'] = this.image; json[r'job'] = this.job; json[r'library'] = this.library_; json[r'logging'] = this.logging; @@ -120,7 +121,6 @@ class SystemConfigDto { json[r'server'] = this.server; json[r'storageTemplate'] = this.storageTemplate; json[r'theme'] = this.theme; - json[r'thumbnail'] = this.thumbnail; json[r'trash'] = this.trash; json[r'user'] = this.user; return json; @@ -135,6 +135,7 @@ class SystemConfigDto { return SystemConfigDto( ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, + image: SystemConfigImageDto.fromJson(json[r'image'])!, job: SystemConfigJobDto.fromJson(json[r'job'])!, library_: SystemConfigLibraryDto.fromJson(json[r'library'])!, logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!, @@ -147,7 +148,6 @@ class SystemConfigDto { server: SystemConfigServerDto.fromJson(json[r'server'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, - thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, user: SystemConfigUserDto.fromJson(json[r'user'])!, ); @@ -198,6 +198,7 @@ class SystemConfigDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'ffmpeg', + 'image', 'job', 'library', 'logging', @@ -210,7 +211,6 @@ class SystemConfigDto { 'server', 'storageTemplate', 'theme', - 'thumbnail', 'trash', 'user', }; diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart new file mode 100644 index 000000000..1c830861a --- /dev/null +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -0,0 +1,138 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigImageDto { + /// Returns a new [SystemConfigImageDto] instance. + SystemConfigImageDto({ + required this.colorspace, + required this.previewFormat, + required this.previewSize, + required this.quality, + required this.thumbnailFormat, + required this.thumbnailSize, + }); + + Colorspace colorspace; + + ImageFormat previewFormat; + + int previewSize; + + int quality; + + ImageFormat thumbnailFormat; + + int thumbnailSize; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && + other.colorspace == colorspace && + other.previewFormat == previewFormat && + other.previewSize == previewSize && + other.quality == quality && + other.thumbnailFormat == thumbnailFormat && + other.thumbnailSize == thumbnailSize; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (colorspace.hashCode) + + (previewFormat.hashCode) + + (previewSize.hashCode) + + (quality.hashCode) + + (thumbnailFormat.hashCode) + + (thumbnailSize.hashCode); + + @override + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + + Map toJson() { + final json = {}; + json[r'colorspace'] = this.colorspace; + json[r'previewFormat'] = this.previewFormat; + json[r'previewSize'] = this.previewSize; + json[r'quality'] = this.quality; + json[r'thumbnailFormat'] = this.thumbnailFormat; + json[r'thumbnailSize'] = this.thumbnailSize; + return json; + } + + /// Returns a new [SystemConfigImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigImageDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigImageDto( + colorspace: Colorspace.fromJson(json[r'colorspace'])!, + previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, + previewSize: mapValueOfType(json, r'previewSize')!, + quality: mapValueOfType(json, r'quality')!, + thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!, + thumbnailSize: mapValueOfType(json, r'thumbnailSize')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigImageDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigImageDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'colorspace', + 'previewFormat', + 'previewSize', + 'quality', + 'thumbnailFormat', + 'thumbnailSize', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart b/mobile/openapi/lib/model/system_config_thumbnail_dto.dart deleted file mode 100644 index f1d55d622..000000000 --- a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart +++ /dev/null @@ -1,122 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class SystemConfigThumbnailDto { - /// Returns a new [SystemConfigThumbnailDto] instance. - SystemConfigThumbnailDto({ - required this.colorspace, - required this.jpegSize, - required this.quality, - required this.webpSize, - }); - - Colorspace colorspace; - - int jpegSize; - - int quality; - - int webpSize; - - @override - bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto && - other.colorspace == colorspace && - other.jpegSize == jpegSize && - other.quality == quality && - other.webpSize == webpSize; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (colorspace.hashCode) + - (jpegSize.hashCode) + - (quality.hashCode) + - (webpSize.hashCode); - - @override - String toString() => 'SystemConfigThumbnailDto[colorspace=$colorspace, jpegSize=$jpegSize, quality=$quality, webpSize=$webpSize]'; - - Map toJson() { - final json = {}; - json[r'colorspace'] = this.colorspace; - json[r'jpegSize'] = this.jpegSize; - json[r'quality'] = this.quality; - json[r'webpSize'] = this.webpSize; - return json; - } - - /// Returns a new [SystemConfigThumbnailDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static SystemConfigThumbnailDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return SystemConfigThumbnailDto( - colorspace: Colorspace.fromJson(json[r'colorspace'])!, - jpegSize: mapValueOfType(json, r'jpegSize')!, - quality: mapValueOfType(json, r'quality')!, - webpSize: mapValueOfType(json, r'webpSize')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = SystemConfigThumbnailDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = SystemConfigThumbnailDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of SystemConfigThumbnailDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = SystemConfigThumbnailDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'colorspace', - 'jpegSize', - 'quality', - 'webpSize', - }; -} - diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index d210d0e4d..0c1729e95 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -95,22 +95,12 @@ void main() { // TODO }); - //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async - test('test getTimeBucket', () async { - // TODO - }); - - //Future> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async - test('test getTimeBuckets', () async { - // TODO - }); - //Future runAssetJobs(AssetJobsDto assetJobsDto) async test('test runAssetJobs', () async { // TODO }); - //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String previewPath, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, String thumbnailPath, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/image_format_test.dart b/mobile/openapi/test/image_format_test.dart new file mode 100644 index 000000000..2bb1512a6 --- /dev/null +++ b/mobile/openapi/test/image_format_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ImageFormat +void main() { + + group('test ImageFormat', () { + + }); + +} diff --git a/mobile/openapi/test/memory_api_test.dart b/mobile/openapi/test/memory_api_test.dart new file mode 100644 index 000000000..1a930782e --- /dev/null +++ b/mobile/openapi/test/memory_api_test.dart @@ -0,0 +1,56 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for MemoryApi +void main() { + // final instance = MemoryApi(); + + group('tests for MemoryApi', () { + //Future> addMemoryAssets(String id, BulkIdsDto bulkIdsDto) async + test('test addMemoryAssets', () async { + // TODO + }); + + //Future createMemory(MemoryCreateDto memoryCreateDto) async + test('test createMemory', () async { + // TODO + }); + + //Future deleteMemory(String id) async + test('test deleteMemory', () async { + // TODO + }); + + //Future getMemory(String id) async + test('test getMemory', () async { + // TODO + }); + + //Future> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto) async + test('test removeMemoryAssets', () async { + // TODO + }); + + //Future> searchMemories() async + test('test searchMemories', () async { + // TODO + }); + + //Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto) async + test('test updateMemory', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/memory_create_dto_test.dart b/mobile/openapi/test/memory_create_dto_test.dart new file mode 100644 index 000000000..f2909bd46 --- /dev/null +++ b/mobile/openapi/test/memory_create_dto_test.dart @@ -0,0 +1,52 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryCreateDto +void main() { + // final instance = MemoryCreateDto(); + + group('test MemoryCreateDto', () { + // List assetIds (default value: const []) + test('to test the property `assetIds`', () async { + // TODO + }); + + // Object data + test('to test the property `data`', () async { + // TODO + }); + + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + // MemoryType type + test('to test the property `type`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/memory_lane_response_dto_test.dart b/mobile/openapi/test/memory_lane_response_dto_test.dart index 2dad2c356..4ed84f5ec 100644 --- a/mobile/openapi/test/memory_lane_response_dto_test.dart +++ b/mobile/openapi/test/memory_lane_response_dto_test.dart @@ -26,6 +26,11 @@ void main() { // TODO }); + // int yearsAgo + test('to test the property `yearsAgo`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/memory_response_dto_test.dart b/mobile/openapi/test/memory_response_dto_test.dart new file mode 100644 index 000000000..da25bbb6e --- /dev/null +++ b/mobile/openapi/test/memory_response_dto_test.dart @@ -0,0 +1,77 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryResponseDto +void main() { + // final instance = MemoryResponseDto(); + + group('test MemoryResponseDto', () { + // List assets (default value: const []) + test('to test the property `assets`', () async { + // TODO + }); + + // DateTime createdAt + test('to test the property `createdAt`', () async { + // TODO + }); + + // Object data + test('to test the property `data`', () async { + // TODO + }); + + // DateTime deletedAt + test('to test the property `deletedAt`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // String ownerId + test('to test the property `ownerId`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + // String type + test('to test the property `type`', () async { + // TODO + }); + + // DateTime updatedAt + test('to test the property `updatedAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/memory_type_test.dart b/mobile/openapi/test/memory_type_test.dart new file mode 100644 index 000000000..0a6589d9a --- /dev/null +++ b/mobile/openapi/test/memory_type_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryType +void main() { + + group('test MemoryType', () { + + }); + +} diff --git a/mobile/openapi/test/memory_update_dto_test.dart b/mobile/openapi/test/memory_update_dto_test.dart new file mode 100644 index 000000000..173128e39 --- /dev/null +++ b/mobile/openapi/test/memory_update_dto_test.dart @@ -0,0 +1,37 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryUpdateDto +void main() { + // final instance = MemoryUpdateDto(); + + group('test MemoryUpdateDto', () { + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/metadata_search_dto_test.dart b/mobile/openapi/test/metadata_search_dto_test.dart index f817b7da7..62979da9c 100644 --- a/mobile/openapi/test/metadata_search_dto_test.dart +++ b/mobile/openapi/test/metadata_search_dto_test.dart @@ -151,6 +151,11 @@ void main() { // TODO }); + // String previewPath + test('to test the property `previewPath`', () async { + // TODO + }); + // String resizePath test('to test the property `resizePath`', () async { // TODO @@ -176,6 +181,11 @@ void main() { // TODO }); + // String thumbnailPath + test('to test the property `thumbnailPath`', () async { + // TODO + }); + // DateTime trashedAfter test('to test the property `trashedAfter`', () async { // TODO diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index aa4a94847..801c97a18 100644 --- a/mobile/openapi/test/search_api_test.dart +++ b/mobile/openapi/test/search_api_test.dart @@ -17,6 +17,11 @@ void main() { // final instance = SearchApi(); group('tests for SearchApi', () { + //Future> getAssetsByCity() async + test('test getAssetsByCity', () async { + // TODO + }); + //Future> getExploreData() async test('test getExploreData', () async { // TODO diff --git a/mobile/openapi/test/system_config_api_test.dart b/mobile/openapi/test/system_config_api_test.dart index fcf1ecfc5..0330d6a3d 100644 --- a/mobile/openapi/test/system_config_api_test.dart +++ b/mobile/openapi/test/system_config_api_test.dart @@ -27,7 +27,7 @@ void main() { // TODO }); - //Future getMapStyle(MapTheme theme) async + //Future getMapStyle(MapTheme theme, { String key }) async test('test getMapStyle', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index b41d07e5f..e88ee17c4 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // SystemConfigImageDto image + test('to test the property `image`', () async { + // TODO + }); + // SystemConfigJobDto job test('to test the property `job`', () async { // TODO @@ -81,11 +86,6 @@ void main() { // TODO }); - // SystemConfigThumbnailDto thumbnail - test('to test the property `thumbnail`', () async { - // TODO - }); - // SystemConfigTrashDto trash test('to test the property `trash`', () async { // TODO diff --git a/mobile/openapi/test/system_config_thumbnail_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart similarity index 53% rename from mobile/openapi/test/system_config_thumbnail_dto_test.dart rename to mobile/openapi/test/system_config_image_dto_test.dart index 3cc66f467..aef907bbe 100644 --- a/mobile/openapi/test/system_config_thumbnail_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -11,18 +11,23 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for SystemConfigThumbnailDto +// tests for SystemConfigImageDto void main() { - // final instance = SystemConfigThumbnailDto(); + // final instance = SystemConfigImageDto(); - group('test SystemConfigThumbnailDto', () { + group('test SystemConfigImageDto', () { // Colorspace colorspace test('to test the property `colorspace`', () async { // TODO }); - // int jpegSize - test('to test the property `jpegSize`', () async { + // ImageFormat previewFormat + test('to test the property `previewFormat`', () async { + // TODO + }); + + // int previewSize + test('to test the property `previewSize`', () async { // TODO }); @@ -31,8 +36,13 @@ void main() { // TODO }); - // int webpSize - test('to test the property `webpSize`', () async { + // ImageFormat thumbnailFormat + test('to test the property `thumbnailFormat`', () async { + // TODO + }); + + // int thumbnailSize + test('to test the property `thumbnailSize`', () async { // TODO }); diff --git a/mobile/openapi/test/timeline_api_test.dart b/mobile/openapi/test/timeline_api_test.dart new file mode 100644 index 000000000..ae217b2e4 --- /dev/null +++ b/mobile/openapi/test/timeline_api_test.dart @@ -0,0 +1,31 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for TimelineApi +void main() { + // final instance = TimelineApi(); + + group('tests for TimelineApi', () { + //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async + test('test getTimeBucket', () async { + // TODO + }); + + //Future> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async + test('test getTimeBuckets', () async { + // TODO + }); + + }); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f7a57bb2b..6bf2b0902 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1,1744 +1,1808 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" - url: "https://pub.dev" - source: hosted - version: "66.0.0" - analyzer: - dependency: "direct overridden" - description: - name: analyzer - sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" - url: "https://pub.dev" - source: hosted - version: "6.4.0" - analyzer_plugin: - dependency: "direct overridden" - description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" - url: "https://pub.dev" - source: hosted - version: "0.11.3" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - archive: - dependency: transitive - description: - name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" - url: "https://pub.dev" - source: hosted - version: "3.4.10" - args: - dependency: transitive - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: "direct main" - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - auto_route: - dependency: "direct main" - description: - name: auto_route - sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1" - url: "https://pub.dev" - source: hosted - version: "7.8.4" - auto_route_generator: - dependency: "direct dev" - description: - name: auto_route_generator - sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22" - url: "https://pub.dev" - source: hosted - version: "7.3.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" - url: "https://pub.dev" - source: hosted - version: "2.4.8" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" - url: "https://pub.dev" - source: hosted - version: "7.2.10" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" - url: "https://pub.dev" - source: hosted - version: "8.6.1" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - cancellation_token: - dependency: transitive - description: - name: cancellation_token - sha256: ad95acf9d4b2f3563e25dc937f63587e46a70ce534e910b65d10e115490f1027 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - cancellation_token_http: - dependency: "direct main" - description: - name: cancellation_token_http - sha256: bb91655e2e47d6274b681261ee6a687b7aa9023f49cfc28f42d095b2f86febc3 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" - url: "https://pub.dev" - source: hosted - version: "1.7.4" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 - url: "https://pub.dev" - source: hosted - version: "0.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" - url: "https://pub.dev" - source: hosted - version: "4.5.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - connectivity_plus: - dependency: "direct main" - description: - name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.dev" - source: hosted - version: "1.2.4" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" - url: "https://pub.dev" - source: hosted - version: "0.3.3+4" - crypto: - dependency: transitive - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" - source: hosted - version: "1.0.5" - custom_lint: - dependency: "direct dev" - description: - name: custom_lint - sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" - url: "https://pub.dev" - source: hosted - version: "0.5.11" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: "7d0b094266b5c357769fffb920826b1a08373290f3c5c44b86253aae6873d5fc" - url: "https://pub.dev" - source: hosted - version: "0.5.11" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "77dd37e9afe5ed86fc75de22ed3dfae5afb75303111358766550af23e7de53cd" - url: "https://pub.dev" - source: hosted - version: "0.5.11" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" - url: "https://pub.dev" - source: hosted - version: "0.7.8" - device_info_plus: - dependency: "direct main" - description: - name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" - url: "https://pub.dev" - source: hosted - version: "9.1.1" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - easy_image_viewer: - dependency: "direct main" - description: - name: easy_image_viewer - sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - easy_localization: - dependency: "direct main" - description: - name: easy_localization - sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - easy_logger: - dependency: transitive - description: - name: easy_logger - sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 - url: "https://pub.dev" - source: hosted - version: "0.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" - url: "https://pub.dev" - source: hosted - version: "0.9.2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" - url: "https://pub.dev" - source: hosted - version: "0.9.3+1" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" - url: "https://pub.dev" - source: hosted - version: "0.9.3" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - flutter_displaymode: - dependency: "direct main" - description: - name: flutter_displaymode - sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef" - url: "https://pub.dev" - source: hosted - version: "0.6.0" - flutter_driver: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_hooks: - dependency: "direct main" - description: - name: flutter_hooks - sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" - url: "https://pub.dev" - source: hosted - version: "0.20.4" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" - url: "https://pub.dev" - source: hosted - version: "0.13.1" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 - url: "https://pub.dev" - source: hosted - version: "16.3.2" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" - url: "https://pub.dev" - source: hosted - version: "4.0.0+1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" - url: "https://pub.dev" - source: hosted - version: "7.0.0+1" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_native_splash: - dependency: "direct dev" - description: - name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" - url: "https://pub.dev" - source: hosted - version: "2.3.10" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" - url: "https://pub.dev" - source: hosted - version: "2.0.15" - flutter_riverpod: - dependency: transitive - description: - name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 - url: "https://pub.dev" - source: hosted - version: "2.4.9" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_udid: - dependency: "direct main" - description: - name: flutter_udid - sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - flutter_web_auth: - dependency: "direct main" - description: - name: flutter_web_auth - sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd - url: "https://pub.dev" - source: hosted - version: "0.5.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - fluttertoast: - dependency: "direct main" - description: - name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 - url: "https://pub.dev" - source: hosted - version: "8.2.4" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d - url: "https://pub.dev" - source: hosted - version: "2.4.1" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - geolocator: - dependency: "direct main" - description: - name: geolocator - sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" - url: "https://pub.dev" - source: hosted - version: "11.0.0" - geolocator_android: - dependency: transitive - description: - name: geolocator_android - sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" - url: "https://pub.dev" - source: hosted - version: "4.3.1" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" - url: "https://pub.dev" - source: hosted - version: "2.3.5" - geolocator_platform_interface: - dependency: transitive - description: - name: geolocator_platform_interface - sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - geolocator_windows: - dependency: transitive - description: - name: geolocator_windows - sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af - url: "https://pub.dev" - source: hosted - version: "0.2.2" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - graphs: - dependency: transitive - description: - name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - hooks_riverpod: - dependency: "direct main" - description: - name: hooks_riverpod - sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae - url: "https://pub.dev" - source: hosted - version: "2.4.9" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - html: - dependency: transitive - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" - http: - dependency: "direct main" - description: - name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.dev" - source: hosted - version: "0.13.5" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: "direct main" - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" - url: "https://pub.dev" - source: hosted - version: "4.1.4" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" - url: "https://pub.dev" - source: hosted - version: "1.0.7" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" - url: "https://pub.dev" - source: hosted - version: "0.8.7+4" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b - url: "https://pub.dev" - source: hosted - version: "0.8.8" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 - url: "https://pub.dev" - source: hosted - version: "0.2.1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 - url: "https://pub.dev" - source: hosted - version: "2.9.0" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 - url: "https://pub.dev" - source: hosted - version: "0.2.1" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.dev" - source: hosted - version: "0.18.1" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - isar: - dependency: "direct main" - description: - name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_flutter_libs: - dependency: "direct main" - description: - name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_generator: - dependency: "direct dev" - description: - name: isar_generator - sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - maplibre_gl: - dependency: "direct main" - description: - path: "." - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - maplibre_gl_platform_interface: - dependency: transitive - description: - path: maplibre_gl_platform_interface - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - maplibre_gl_web: - dependency: transitive - description: - path: maplibre_gl_web - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - meta: - dependency: "direct overridden" - description: - name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 - url: "https://pub.dev" - source: hosted - version: "1.11.0" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - mocktail: - dependency: "direct dev" - description: - name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - nm: - dependency: transitive - description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - octo_image: - dependency: "direct main" - description: - name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - openapi: - dependency: "direct main" - description: - path: openapi - relative: true - source: path - version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" - url: "https://pub.dev" - source: hosted - version: "5.0.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - path: - dependency: "direct main" - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.dev" - source: hosted - version: "1.8.3" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - path_provider_ios: - dependency: "direct main" - description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" - url: "https://pub.dev" - source: hosted - version: "2.0.11" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da - url: "https://pub.dev" - source: hosted - version: "2.2.0" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" - url: "https://pub.dev" - source: hosted - version: "11.2.0" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" - url: "https://pub.dev" - source: hosted - version: "12.0.3" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 - url: "https://pub.dev" - source: hosted - version: "9.3.0" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" - url: "https://pub.dev" - source: hosted - version: "0.1.1" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - photo_manager: - dependency: "direct main" - description: - name: photo_manager - sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" - url: "https://pub.dev" - source: hosted - version: "3.0.0-dev.5" - photo_manager_image_provider: - dependency: "direct main" - description: - name: photo_manager_image_provider - sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 - url: "https://pub.dev" - source: hosted - version: "2.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" - provider: - dependency: transitive - description: - name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f - url: "https://pub.dev" - source: hosted - version: "6.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" - url: "https://pub.dev" - source: hosted - version: "2.4.9" - riverpod_analyzer_utils: - dependency: transitive - description: - name: riverpod_analyzer_utils - sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 - url: "https://pub.dev" - source: hosted - version: "0.5.0" - riverpod_annotation: - dependency: "direct main" - description: - name: riverpod_annotation - sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 - url: "https://pub.dev" - source: hosted - version: "2.3.3" - riverpod_generator: - dependency: "direct dev" - description: - name: riverpod_generator - sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 - url: "https://pub.dev" - source: hosted - version: "2.3.9" - riverpod_lint: - dependency: "direct dev" - description: - name: riverpod_lint - sha256: "944929ef82c9bfeaa455ccab97920abcf847a0ffed5c9f6babc520a95db25176" - url: "https://pub.dev" - source: hosted - version: "2.3.7" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" - source: hosted - version: "0.3.8" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd - url: "https://pub.dev" - source: hosted - version: "7.2.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 - url: "https://pub.dev" - source: hosted - version: "3.3.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef - url: "https://pub.dev" - source: hosted - version: "2.3.3" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - socket_io_client: - dependency: "direct main" - description: - name: socket_io_client - sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b - url: "https://pub.dev" - source: hosted - version: "2.0.3+1" - socket_io_common: - dependency: transitive - description: - name: socket_io_common - sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" - url: "https://pub.dev" - source: hosted - version: "2.0.3" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - state_notifier: - dependency: transitive - description: - name: state_notifier - sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" - source: hosted - version: "1.0.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - thumbhash: - dependency: "direct main" - description: - name: thumbhash - sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" - url: "https://pub.dev" - source: hosted - version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - timezone: - dependency: "direct main" - description: - name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" - url: "https://pub.dev" - source: hosted - version: "0.9.2" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c - url: "https://pub.dev" - source: hosted - version: "6.2.4" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" - url: "https://pub.dev" - source: hosted - version: "6.2.2" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" - url: "https://pub.dev" - source: hosted - version: "6.2.4" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 - url: "https://pub.dev" - source: hosted - version: "3.1.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b - url: "https://pub.dev" - source: hosted - version: "2.2.3" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 - url: "https://pub.dev" - source: hosted - version: "3.1.1" - uuid: - dependency: transitive - description: - name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.dev" - source: hosted - version: "3.0.7" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 - url: "https://pub.dev" - source: hosted - version: "2.8.2" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 - url: "https://pub.dev" - source: hosted - version: "2.4.9" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" - url: "https://pub.dev" - source: hosted - version: "2.0.16" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 - url: "https://pub.dev" - source: hosted - version: "11.10.0" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d - url: "https://pub.dev" - source: hosted - version: "1.1.4" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webdriver: - dependency: transitive - description: - name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - win32: - dependency: transitive - description: - name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" - url: "https://pub.dev" - source: hosted - version: "4.1.4" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 - url: "https://pub.dev" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + url: "https://pub.dev" + source: hosted + version: "66.0.0" + analyzer: + dependency: "direct overridden" + description: + name: analyzer + sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + analyzer_plugin: + dependency: "direct overridden" + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: "direct main" + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1" + url: "https://pub.dev" + source: hosted + version: "7.8.4" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22" + url: "https://pub.dev" + source: hosted + version: "7.3.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cancellation_token: + dependency: transitive + description: + name: cancellation_token + sha256: ad95acf9d4b2f3563e25dc937f63587e46a70ce534e910b65d10e115490f1027 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + cancellation_token_http: + dependency: "direct main" + description: + name: cancellation_token_http + sha256: "37ad2a20dba02aeb1f0a4d845e7a57eebacdb709e1186e0491e7cd81c559c4ff" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + url: "https://pub.dev" + source: hosted + version: "1.7.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + easy_image_viewer: + dependency: "direct main" + description: + name: easy_image_viewer + sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + url: "https://pub.dev" + source: hosted + version: "0.20.4" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + url: "https://pub.dev" + source: hosted + version: "16.3.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + url: "https://pub.dev" + source: hosted + version: "2.3.10" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_udid: + dependency: "direct main" + description: + name: flutter_udid + sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_web_auth: + dependency: "direct main" + description: + name: flutter_web_auth + sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd + url: "https://pub.dev" + source: hosted + version: "0.5.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + url: "https://pub.dev" + source: hosted + version: "8.2.4" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" + url: "https://pub.dev" + source: hosted + version: "11.0.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" + url: "https://pub.dev" + source: hosted + version: "4.3.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + url: "https://pub.dev" + source: hosted + version: "0.2.2" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: "direct main" + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + url: "https://pub.dev" + source: hosted + version: "0.8.7+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + url: "https://pub.dev" + source: hosted + version: "0.8.8" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + url: "https://pub.dev" + source: hosted + version: "2.9.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + isar: + dependency: "direct main" + description: + name: isar + sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_flutter_libs: + dependency: "direct main" + description: + name: isar_flutter_libs + sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_generator: + dependency: "direct dev" + description: + name: isar_generator + sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + maplibre_gl: + dependency: "direct main" + description: + path: "." + ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_platform_interface: + dependency: transitive + description: + path: maplibre_gl_platform_interface + ref: main + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_web: + dependency: transitive + description: + path: maplibre_gl_web + ref: main + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: "direct overridden" + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: "direct main" + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + openapi: + dependency: "direct main" + description: + path: openapi + relative: true + source: path + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + path_provider_ios: + dependency: "direct main" + description: + name: path_provider_ios + sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + url: "https://pub.dev" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + url: "https://pub.dev" + source: hosted + version: "2.2.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + url: "https://pub.dev" + source: hosted + version: "11.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + url: "https://pub.dev" + source: hosted + version: "9.3.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + photo_manager: + dependency: "direct main" + description: + name: photo_manager + sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.5" + photo_manager_image_provider: + dependency: "direct main" + description: + name: photo_manager_image_provider + sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + provider: + dependency: transitive + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + url: "https://pub.dev" + source: hosted + version: "2.5.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 + url: "https://pub.dev" + source: hosted + version: "2.3.9" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + url: "https://pub.dev" + source: hosted + version: "2.3.10" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + url: "https://pub.dev" + source: hosted + version: "3.3.1" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socket_io_client: + dependency: "direct main" + description: + name: socket_io_client + sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b + url: "https://pub.dev" + source: hosted + version: "2.0.3+1" + socket_io_common: + dependency: transitive + description: + name: socket_io_common + sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + thumbhash: + dependency: "direct main" + description: + name: thumbhash + sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" + time: + dependency: transitive + description: + name: time + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + url: "https://pub.dev" + source: hosted + version: "2.8.2" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + url: "https://pub.dev" + source: hosted + version: "2.4.9" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + url: "https://pub.dev" + source: hosted + version: "1.1.4" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index ed8a4fad6..46cfe872b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,8 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.98.2+128 -isar_version: &isar_version 3.1.0+1 +version: 1.100.0+130 environment: sdk: '>=3.0.0 <4.0.0' @@ -34,12 +33,13 @@ dependencies: ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 geolocator: ^11.0.0 # used to move to current location in map view flutter_udid: ^3.0.0 + flutter_svg: ^2.0.9 package_info_plus: ^5.0.1 url_launcher: ^6.2.4 - http: 0.13.5 - cancellation_token_http: ^1.1.0 + http: ^0.13.6 + cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 - share_plus: ^7.2.1 + share_plus: ^7.2.2 flutter_displaymode: ^0.6.0 scrollable_positioned_list: ^0.3.8 path: ^1.8.3 @@ -48,8 +48,8 @@ dependencies: http_parser: ^4.0.2 flutter_web_auth: ^0.5.0 easy_image_viewer: ^1.4.0 - isar: *isar_version - isar_flutter_libs: *isar_version # contains Isar Core + isar: ^3.1.0+1 + isar_flutter_libs: ^3.1.0+1 permission_handler: ^11.2.0 device_info_plus: ^9.1.1 connectivity_plus: ^5.0.2 @@ -90,10 +90,10 @@ dev_dependencies: auto_route_generator: ^7.3.2 flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.3.9 - isar_generator: *isar_version + isar_generator: ^3.1.0+1 integration_test: sdk: flutter - custom_lint: ^0.5.8 + custom_lint: ^0.6.0 riverpod_lint: ^2.3.7 riverpod_generator: ^2.3.9 mocktail: ^1.0.3 diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 82562100a..da7bad8f0 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1720,286 +1720,6 @@ ] } }, - "/asset/time-bucket": { - "get": { - "operationId": "getTimeBucket", - "parameters": [ - { - "name": "albumId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - { - "name": "personId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "size", - "required": true, - "in": "query", - "schema": { - "$ref": "#/components/schemas/TimeBucketSize" - } - }, - { - "name": "timeBucket", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "userId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "withPartners", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "withStacked", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - }, - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, - "/asset/time-buckets": { - "get": { - "operationId": "getTimeBuckets", - "parameters": [ - { - "name": "albumId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - { - "name": "personId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "size", - "required": true, - "in": "query", - "schema": { - "$ref": "#/components/schemas/TimeBucketSize" - } - }, - { - "name": "userId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "withPartners", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "withStacked", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/TimeBucketResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - }, - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/upload": { "post": { "operationId": "uploadFile", @@ -2381,10 +2101,19 @@ } } }, + { + "name": "previewPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "resizePath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -2423,6 +2152,14 @@ "type": "string" } }, + { + "name": "thumbnailPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "trashedAfter", "required": false, @@ -2471,6 +2208,7 @@ "name": "webpPath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -3697,6 +3435,314 @@ ] } }, + "/memories": { + "get": { + "operationId": "searchMemories", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MemoryResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "post": { + "operationId": "createMemory", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, + "/memories/{id}": { + "delete": { + "operationId": "deleteMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "get": { + "operationId": "getMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "put": { + "operationId": "updateMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, + "/memories/{id}/assets": { + "delete": { + "operationId": "removeMemoryAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "put": { + "operationId": "addMemoryAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, "/oauth/authorize": { "post": { "operationId": "startOAuth", @@ -4597,6 +4643,41 @@ ] } }, + "/search/cities": { + "get": { + "operationId": "getAssetsByCity", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/explore": { "get": { "operationId": "getExploreData", @@ -5593,6 +5674,14 @@ "get": { "operationId": "getMapStyle", "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "theme", "required": true, @@ -6013,6 +6102,286 @@ ] } }, + "/timeline/bucket": { + "get": { + "operationId": "getTimeBucket", + "parameters": [ + { + "name": "albumId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isTrashed", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "personId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "size", + "required": true, + "in": "query", + "schema": { + "$ref": "#/components/schemas/TimeBucketSize" + } + }, + { + "name": "timeBucket", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + }, + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Timeline" + ] + } + }, + "/timeline/buckets": { + "get": { + "operationId": "getTimeBuckets", + "parameters": [ + { + "name": "albumId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isTrashed", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "personId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "size", + "required": true, + "in": "query", + "schema": { + "$ref": "#/components/schemas/TimeBucketSize" + } + }, + { + "name": "userId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TimeBucketResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + }, + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Timeline" + ] + } + }, "/trash/empty": { "post": { "operationId": "emptyTrash", @@ -6503,7 +6872,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.98.2", + "version": "1.100.0", "contact": {} }, "tags": [], @@ -8071,6 +8440,13 @@ ], "type": "object" }, + "ImageFormat": { + "enum": [ + "jpeg", + "webp" + ], + "type": "string" + }, "JobCommand": { "enum": [ "start", @@ -8383,6 +8759,40 @@ ], "type": "string" }, + "MemoryCreateDto": { + "properties": { + "assetIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "data": { + "type": "object" + }, + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/MemoryType" + } + }, + "required": [ + "data", + "memoryAt", + "type" + ], + "type": "object" + }, "MemoryLaneResponseDto": { "properties": { "assets": { @@ -8392,15 +8802,102 @@ "type": "array" }, "title": { + "deprecated": true, + "type": "string" + }, + "yearsAgo": { + "type": "integer" + } + }, + "required": [ + "assets", + "title", + "yearsAgo" + ], + "type": "object" + }, + "MemoryResponseDto": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "data": { + "type": "object" + }, + "deletedAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + }, + "type": { + "enum": [ + "on_this_day" + ], + "type": "string" + }, + "updatedAt": { + "format": "date-time", "type": "string" } }, "required": [ "assets", - "title" + "createdAt", + "data", + "id", + "isSaved", + "memoryAt", + "ownerId", + "type", + "updatedAt" ], "type": "object" }, + "MemoryType": { + "enum": [ + "on_this_day" + ], + "type": "string" + }, + "MemoryUpdateDto": { + "properties": { + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, "MergePersonDto": { "properties": { "ids": { @@ -8507,7 +9004,11 @@ }, "type": "array" }, + "previewPath": { + "type": "string" + }, "resizePath": { + "deprecated": true, "type": "string" }, "size": { @@ -8524,6 +9025,9 @@ "format": "date-time", "type": "string" }, + "thumbnailPath": { + "type": "string" + }, "trashedAfter": { "format": "date-time", "type": "string" @@ -8544,6 +9048,7 @@ "type": "string" }, "webpPath": { + "deprecated": true, "type": "string" }, "withArchived": { @@ -8698,8 +9203,8 @@ "PathType": { "enum": [ "original", - "jpeg_thumbnail", - "webp_thumbnail", + "preview", + "thumbnail", "encoded_video", "sidecar", "face", @@ -9695,6 +10200,9 @@ "ffmpeg": { "$ref": "#/components/schemas/SystemConfigFFmpegDto" }, + "image": { + "$ref": "#/components/schemas/SystemConfigImageDto" + }, "job": { "$ref": "#/components/schemas/SystemConfigJobDto" }, @@ -9731,9 +10239,6 @@ "theme": { "$ref": "#/components/schemas/SystemConfigThemeDto" }, - "thumbnail": { - "$ref": "#/components/schemas/SystemConfigThumbnailDto" - }, "trash": { "$ref": "#/components/schemas/SystemConfigTrashDto" }, @@ -9743,6 +10248,7 @@ }, "required": [ "ffmpeg", + "image", "job", "library", "logging", @@ -9755,7 +10261,6 @@ "server", "storageTemplate", "theme", - "thumbnail", "trash", "user" ], @@ -9854,6 +10359,37 @@ ], "type": "object" }, + "SystemConfigImageDto": { + "properties": { + "colorspace": { + "$ref": "#/components/schemas/Colorspace" + }, + "previewFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "previewSize": { + "type": "integer" + }, + "quality": { + "type": "integer" + }, + "thumbnailFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "thumbnailSize": { + "type": "integer" + } + }, + "required": [ + "colorspace", + "previewFormat", + "previewSize", + "quality", + "thumbnailFormat", + "thumbnailSize" + ], + "type": "object" + }, "SystemConfigJobDto": { "properties": { "backgroundTask": { @@ -10203,29 +10739,6 @@ ], "type": "object" }, - "SystemConfigThumbnailDto": { - "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" - }, - "jpegSize": { - "type": "integer" - }, - "quality": { - "type": "integer" - }, - "webpSize": { - "type": "integer" - } - }, - "required": [ - "colorspace", - "jpegSize", - "quality", - "webpSize" - ], - "type": "object" - }, "SystemConfigTrashDto": { "properties": { "days": { diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 0f1926996..dc900ff52 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" @@ -22,18 +22,18 @@ "integrity": "sha512-V33FjR6V+AkGRWYQW3XPm5BLn2loGl2ujSeja1TzdjjEn2zjGgl3ve0dcFf/jEwPZEOqQZl6YwIgIB/clXVqWw==" }, "node_modules/@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 4b04ee7c2..8fee0fc0f 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 6b5064252..1584a79cf 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.98.2 + * 1.100.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -273,6 +273,7 @@ export type MapMarkerResponseDto = { export type MemoryLaneResponseDto = { assets: AssetResponseDto[]; title: string; + yearsAgo: number; }; export type UpdateStackParentDto = { newParentId: string; @@ -283,10 +284,6 @@ export type AssetStatsResponseDto = { total: number; videos: number; }; -export type TimeBucketResponseDto = { - count: number; - timeBucket: string; -}; export type CreateAssetDto = { assetData: Blob; deviceAssetId: string; @@ -497,6 +494,32 @@ export type ValidateLibraryImportPathResponseDto = { export type ValidateLibraryResponseDto = { importPaths?: ValidateLibraryImportPathResponseDto[]; }; +export type MemoryResponseDto = { + assets: AssetResponseDto[]; + createdAt: string; + data: object; + deletedAt?: string; + id: string; + isSaved: boolean; + memoryAt: string; + ownerId: string; + seenAt?: string; + "type": Type2; + updatedAt: string; +}; +export type MemoryCreateDto = { + assetIds?: string[]; + data: object; + isSaved?: boolean; + memoryAt: string; + seenAt?: string; + "type": MemoryType; +}; +export type MemoryUpdateDto = { + isSaved?: boolean; + memoryAt?: string; + seenAt?: string; +}; export type OAuthConfigDto = { redirectUri: string; }; @@ -643,11 +666,13 @@ export type MetadataSearchDto = { originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; "type"?: AssetTypeEnum; @@ -830,6 +855,14 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigImageDto = { + colorspace: Colorspace; + previewFormat: ImageFormat; + previewSize: number; + quality: number; + thumbnailFormat: ImageFormat; + thumbnailSize: number; +}; export type JobSettingsDto = { concurrency: number; }; @@ -922,12 +955,6 @@ export type SystemConfigStorageTemplateDto = { export type SystemConfigThemeDto = { customCss: string; }; -export type SystemConfigThumbnailDto = { - colorspace: Colorspace; - jpegSize: number; - quality: number; - webpSize: number; -}; export type SystemConfigTrashDto = { days: number; enabled: boolean; @@ -937,6 +964,7 @@ export type SystemConfigUserDto = { }; export type SystemConfigDto = { ffmpeg: SystemConfigFFmpegDto; + image: SystemConfigImageDto; job: SystemConfigJobDto; library: SystemConfigLibraryDto; logging: SystemConfigLoggingDto; @@ -949,7 +977,6 @@ export type SystemConfigDto = { server: SystemConfigServerDto; storageTemplate: SystemConfigStorageTemplateDto; theme: SystemConfigThemeDto; - thumbnail: SystemConfigThumbnailDto; trash: SystemConfigTrashDto; user: SystemConfigUserDto; }; @@ -970,6 +997,10 @@ export type CreateTagDto = { export type UpdateTagDto = { name?: string; }; +export type TimeBucketResponseDto = { + count: number; + timeBucket: string; +}; export type CreateUserDto = { email: string; memoriesEnabled?: boolean; @@ -1455,72 +1486,6 @@ export function getAssetThumbnail({ format, id, key }: { ...opts })); } -export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { - albumId?: string; - isArchived?: boolean; - isFavorite?: boolean; - isTrashed?: boolean; - key?: string; - order?: AssetOrder; - personId?: string; - size: TimeBucketSize; - timeBucket: string; - userId?: string; - withPartners?: boolean; - withStacked?: boolean; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>(`/asset/time-bucket${QS.query(QS.explode({ - albumId, - isArchived, - isFavorite, - isTrashed, - key, - order, - personId, - size, - timeBucket, - userId, - withPartners, - withStacked - }))}`, { - ...opts - })); -} -export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { - albumId?: string; - isArchived?: boolean; - isFavorite?: boolean; - isTrashed?: boolean; - key?: string; - order?: AssetOrder; - personId?: string; - size: TimeBucketSize; - userId?: string; - withPartners?: boolean; - withStacked?: boolean; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: TimeBucketResponseDto[]; - }>(`/asset/time-buckets${QS.query(QS.explode({ - albumId, - isArchived, - isFavorite, - isTrashed, - key, - order, - personId, - size, - userId, - withPartners, - withStacked - }))}`, { - ...opts - })); -} export function uploadFile({ key, createAssetDto }: { key?: string; createAssetDto: CreateAssetDto; @@ -1562,7 +1527,7 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } -export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { +export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { checksum?: string; city?: string; country?: string; @@ -1590,11 +1555,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; $type?: AssetTypeEnum; @@ -1638,11 +1605,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath, page, personIds, + previewPath, resizePath, size, state, takenAfter, takenBefore, + thumbnailPath, trashedAfter, trashedBefore, "type": $type, @@ -1965,6 +1934,83 @@ export function validate({ id, validateLibraryDto }: { body: validateLibraryDto }))); } +export function searchMemories(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto[]; + }>("/memories", { + ...opts + })); +} +export function createMemory({ memoryCreateDto }: { + memoryCreateDto: MemoryCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: MemoryResponseDto; + }>("/memories", oazapfts.json({ + ...opts, + method: "POST", + body: memoryCreateDto + }))); +} +export function deleteMemory({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/memories/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getMemory({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto; + }>(`/memories/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateMemory({ id, memoryUpdateDto }: { + id: string; + memoryUpdateDto: MemoryUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto; + }>(`/memories/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: memoryUpdateDto + }))); +} +export function removeMemoryAssets({ id, bulkIdsDto }: { + id: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/memories/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} +export function addMemoryAssets({ id, bulkIdsDto }: { + id: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/memories/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "PUT", + body: bulkIdsDto + }))); +} export function startOAuth({ oAuthConfigDto }: { oAuthConfigDto: OAuthConfigDto; }, opts?: Oazapfts.RequestOpts) { @@ -2204,6 +2250,14 @@ export function search({ clip, motion, page, q, query, recent, size, smart, $typ ...opts })); } +export function getAssetsByCity(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>("/search/cities", { + ...opts + })); +} export function getExploreData(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2479,13 +2533,15 @@ export function getConfigDefaults(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function getMapStyle({ theme }: { +export function getMapStyle({ key, theme }: { + key?: string; theme: MapTheme; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: object; }>(`/system-config/map/style.json${QS.query(QS.explode({ + key, theme }))}`, { ...opts @@ -2586,6 +2642,72 @@ export function tagAssets({ id, assetIdsDto }: { body: assetIdsDto }))); } +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { + albumId?: string; + isArchived?: boolean; + isFavorite?: boolean; + isTrashed?: boolean; + key?: string; + order?: AssetOrder; + personId?: string; + size: TimeBucketSize; + timeBucket: string; + userId?: string; + withPartners?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/timeline/bucket${QS.query(QS.explode({ + albumId, + isArchived, + isFavorite, + isTrashed, + key, + order, + personId, + size, + timeBucket, + userId, + withPartners, + withStacked + }))}`, { + ...opts + })); +} +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { + albumId?: string; + isArchived?: boolean; + isFavorite?: boolean; + isTrashed?: boolean; + key?: string; + order?: AssetOrder; + personId?: string; + size: TimeBucketSize; + userId?: string; + withPartners?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TimeBucketResponseDto[]; + }>(`/timeline/buckets${QS.query(QS.explode({ + albumId, + isArchived, + isFavorite, + isTrashed, + key, + order, + personId, + size, + userId, + withPartners, + withStacked + }))}`, { + ...opts + })); +} export function emptyTrash(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchText("/trash/empty", { ...opts, @@ -2780,10 +2902,6 @@ export enum ThumbnailFormat { Jpeg = "JPEG", Webp = "WEBP" } -export enum TimeBucketSize { - Day = "DAY", - Month = "MONTH" -} export enum EntityType { Asset = "ASSET", Album = "ALBUM" @@ -2795,8 +2913,8 @@ export enum PathEntityType { } export enum PathType { Original = "original", - JpegThumbnail = "jpeg_thumbnail", - WebpThumbnail = "webp_thumbnail", + Preview = "preview", + Thumbnail = "thumbnail", EncodedVideo = "encoded_video", Sidecar = "sidecar", Face = "face", @@ -2827,6 +2945,12 @@ export enum LibraryType { Upload = "UPLOAD", External = "EXTERNAL" } +export enum Type2 { + OnThisDay = "on_this_day" +} +export enum MemoryType { + OnThisDay = "on_this_day" +} export enum SearchSuggestionType { Country = "country", State = "state", @@ -2878,6 +3002,14 @@ export enum TranscodePolicy { Required = "required", Disabled = "disabled" } +export enum Colorspace { + Srgb = "srgb", + P3 = "p3" +} +export enum ImageFormat { + Jpeg = "jpeg", + Webp = "webp" +} export enum LogLevel { Verbose = "verbose", Debug = "debug", @@ -2894,11 +3026,11 @@ export enum ModelType { FacialRecognition = "facial-recognition", Clip = "clip" } -export enum Colorspace { - Srgb = "srgb", - P3 = "p3" -} export enum MapTheme { Light = "light", Dark = "dark" } +export enum TimeBucketSize { + Day = "DAY", + Month = "MONTH" +} diff --git a/README_ca_ES.md b/readme_i18n/README_ca_ES.md similarity index 96% rename from README_ca_ES.md rename to readme_i18n/README_ca_ES.md index cf7bcb4f5..83fd882f7 100644 --- a/README_ca_ES.md +++ b/readme_i18n/README_ca_ES.md @@ -9,16 +9,16 @@

- +

Immich - Solució de còpia de seguretat d'alta rendiment per a fotos i vídeos auto-allotjada


- +

- English + English Español Français Italiano diff --git a/README_de_DE.md b/readme_i18n/README_de_DE.md similarity index 96% rename from README_de_DE.md rename to readme_i18n/README_de_DE.md index 322a58b96..6bf3a4456 100644 --- a/README_de_DE.md +++ b/readme_i18n/README_de_DE.md @@ -9,16 +9,16 @@

- +

Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos


- +

- English + English Català Español Français diff --git a/README_es_ES.md b/readme_i18n/README_es_ES.md similarity index 96% rename from README_es_ES.md rename to readme_i18n/README_es_ES.md index 047c47260..c4c5e8fa9 100644 --- a/README_es_ES.md +++ b/readme_i18n/README_es_ES.md @@ -9,16 +9,16 @@

- +

Immich: Una solución Self-Hosted de copia de seguridad de fotos y videos de alto rendimiento


- +

- English + English Català Français Italiano diff --git a/README_fr_FR.md b/readme_i18n/README_fr_FR.md similarity index 96% rename from README_fr_FR.md rename to readme_i18n/README_fr_FR.md index 3b4c6d428..b734f4eb3 100644 --- a/README_fr_FR.md +++ b/readme_i18n/README_fr_FR.md @@ -9,16 +9,16 @@

- +

Immich - Solution de sauvegarde performante et auto-hébergée des photos et des vidéos


- +

- English + English Català Español Italiano diff --git a/README_it_IT.md b/readme_i18n/README_it_IT.md similarity index 96% rename from README_it_IT.md rename to readme_i18n/README_it_IT.md index 7e8863a93..64bd9c9e4 100644 --- a/README_it_IT.md +++ b/readme_i18n/README_it_IT.md @@ -9,16 +9,16 @@

- +

Immich - Soluzione self-hosted ad alte prestazioni per backup di foto e video


- +

- English + English Català Español Français diff --git a/README_ja_JP.md b/readme_i18n/README_ja_JP.md similarity index 96% rename from README_ja_JP.md rename to readme_i18n/README_ja_JP.md index 90143025a..c7f04453f 100644 --- a/README_ja_JP.md +++ b/readme_i18n/README_ja_JP.md @@ -9,16 +9,16 @@

- +

Immich - 高性能なセルフホスト 写真/ビデオバックアップソリューション


- +

- English + English Català Español Français diff --git a/README_ko_KR.md b/readme_i18n/README_ko_KR.md similarity index 96% rename from README_ko_KR.md rename to readme_i18n/README_ko_KR.md index 3a1599597..8f222854d 100644 --- a/README_ko_KR.md +++ b/readme_i18n/README_ko_KR.md @@ -9,16 +9,16 @@

- +

Immich - 고성능 자체 호스팅 사진 및 동영상 백업 솔루션


- +

- English + English Català Español Français diff --git a/README_nl_NL.md b/readme_i18n/README_nl_NL.md similarity index 96% rename from README_nl_NL.md rename to readme_i18n/README_nl_NL.md index 1eeb41e5a..2214e8b46 100644 --- a/README_nl_NL.md +++ b/readme_i18n/README_nl_NL.md @@ -9,16 +9,16 @@

- +

Immich - Hoogwaardige, self-hosted back-up oplossing voor foto's en video's


- +

- English + English Català Español Français diff --git a/README_ru_RU.md b/readme_i18n/README_ru_RU.md similarity index 98% rename from README_ru_RU.md rename to readme_i18n/README_ru_RU.md index f6d50b1ce..ff063a56b 100644 --- a/README_ru_RU.md +++ b/readme_i18n/README_ru_RU.md @@ -9,12 +9,12 @@

- +

Immich - Высокопроизводительное решение для автономоного создания фото и видео архивов


- +

diff --git a/README_tr_TR.md b/readme_i18n/README_tr_TR.md similarity index 96% rename from README_tr_TR.md rename to readme_i18n/README_tr_TR.md index 6e5c43ee6..13b62e406 100644 --- a/README_tr_TR.md +++ b/readme_i18n/README_tr_TR.md @@ -9,16 +9,16 @@

- +

Immich - Yüksek performanslı, kendine ait barındırılan fotoğraf ve video yedekleme çözümü


- +

- English + English Català Español Français diff --git a/README_zh_CN.md b/readme_i18n/README_zh_CN.md similarity index 84% rename from README_zh_CN.md rename to readme_i18n/README_zh_CN.md index 874b18f09..55e085c85 100644 --- a/README_zh_CN.md +++ b/readme_i18n/README_zh_CN.md @@ -9,7 +9,7 @@

- +

Immich - 高性能的自托管照片和视频备份方案

@@ -17,12 +17,12 @@


- +

- English + English Català Español Français @@ -73,17 +73,22 @@ 规格: 甲骨文免费虚拟机套餐——阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。 ``` +## 活跃度 +![活跃度](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image") + # 功能特性 + | 功能特性 | 移动端 | 网页端 | |---------------------------------------------|--------|--------| | 上传并查看照片和视频 | 是 | 是 | | 软件运行时自动备份 | 是 | N/A | +| 忽略重复的项目 | 是 | 是 | | 选择需要备份的相册 | 是 | N/A | | 下载照片和视频到本地 | 是 | 是 | | 多用户支持 | 是 | 是 | | 相册与共享相册 | 是 | 是 | -| 可拖动的快速导航栏 | 是 | 是 | +| 可拖动的快速滚动条 | 是 | 是 | | 支持RAW格式 | 是 | 是 | | 元数据视图(EXIF、地图) | 是 | 是 | | 通过元数据、对象、人脸和标签进行搜索 | 是 | 是 | @@ -93,6 +98,7 @@ | OAuth 支持 | 是 | 是 | | API Keys | N/A | 是 | | 实况照片备份和查看 | 是 | 是 | +| 支持360度全景图显示 | 否 | 是 | | 用户自定义存储结构 | 是 | 是 | | 公共分享 | 否 | 是 | | 归档与收藏功能 | 是 | 是 | @@ -104,7 +110,6 @@ | 只读相册 | 是 | 是 | | 照片堆叠 | 是 | 是 | - # 支持本项目 我已经致力于本项目并且我将会持续更新文档、新增功能和修复问题。但是独木不成林,我需要您给予我坚持下去的动力。 @@ -126,3 +131,13 @@ + +## Star增长曲线 + + + + + + Star History Chart + + diff --git a/renovate.json b/renovate.json index 7cb136b1a..afa68011d 100644 --- a/renovate.json +++ b/renovate.json @@ -4,33 +4,28 @@ "minimumReleaseAge": "5 days", "packageRules": [ { - "matchFileNames": ["cli/**"], - "groupName": "@immich/cli", - "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" - }, - { - "matchFileNames": ["docs/**"], - "groupName": "docs", - "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" - }, - { - "matchFileNames": ["mobile/**"], - "groupName": "mobile", - "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" - }, - { - "matchFileNames": ["server/**"], - "groupName": "server", + "matchFileNames": [ + "cli/**", + "docs/**", + "e2e/**", + "open-api/**", + "server/**", + "web/**" + ], + "groupName": "typescript-projects", "matchUpdateTypes": ["minor", "patch"], "excludePackagePrefixes": ["exiftool", "reflect-metadata"], "schedule": "on tuesday" }, { - "matchFileNames": ["open-api/**"], - "groupName": "open-api", + "matchFileNames": ["machine-learning/**"], + "groupName": "machine-learning", + "rangeStrategy": "in-range-only", + "schedule": "on tuesday" + }, + { + "matchFileNames": ["mobile/**"], + "groupName": "mobile", "matchUpdateTypes": ["minor", "patch"], "schedule": "on tuesday" }, @@ -45,18 +40,6 @@ "matchPackagePrefixes": ["@sveltejs"], "schedule": "on tuesday" }, - { - "matchFileNames": ["web/**"], - "groupName": "web", - "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" - }, - { - "matchFileNames": ["machine-learning/**"], - "groupName": "machine-learning", - "rangeStrategy": "in-range-only", - "schedule": "on tuesday" - }, { "matchFileNames": [".github/**"], "groupName": "github-actions", @@ -81,9 +64,6 @@ } ], "ignorePaths": ["mobile/openapi/pubspec.yaml"], - "ignoreDeps": [ - "http", - "intl" - ], + "ignoreDeps": ["http", "intl"], "labels": ["dependencies", "renovate"] } diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 3673add3c..75138ff23 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -33,5 +33,6 @@ module.exports = { '@typescript-eslint/require-await': 'error', curly: 2, 'prettier/prettier': 0, + 'no-restricted-imports': ['error', { patterns: [{ group: ['.*'], message: 'Relative imports are not allowed.' }] }], }, }; diff --git a/server/Dockerfile b/server/Dockerfile index 2c66c0af3..19f6c76eb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240312@sha256:3cb168dd87a2b412b25c512ec638a1e7f362e1d3eb8dd19a38d92d4a7c47999c as dev +FROM ghcr.io/immich-app/base-server-dev:20240326@sha256:d945aba864051b30888617f36446f86b72c4bc7ad6476b9dd2aaa0c4c4e3c945 as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -24,7 +24,7 @@ RUN npm prune --omit=dev --omit=optional COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img # web build -FROM node:iron-alpine3.18@sha256:a02826c7340c37a29179152723190bcc3044f933c925f3c2d78abb20f794de3f as web +FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef as web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ @@ -40,7 +40,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240312@sha256:8359fb1acc56580f2b4835e273293fdaa99d273b210892e1485fc6f1e47cf2bb +FROM ghcr.io/immich-app/base-server-prod:20240326@sha256:28ad98fed8d746b5f92de49ff776cfdff7399df163ebeda2f37a01f473965841 WORKDIR /usr/src/app ENV NODE_ENV=production \ @@ -53,7 +53,7 @@ COPY --from=web /usr/src/app/build ./www COPY server/resources resources COPY server/package.json server/package-lock.json ./ COPY server/start*.sh ./ -RUN npm link && npm cache clean --force +RUN npm link && npm install -g @immich/cli && npm cache clean --force COPY LICENSE /licenses/LICENSE.txt COPY LICENSE /LICENSE ENV PATH="${PATH}:/usr/src/app/bin" diff --git a/server/README.md b/server/README.md new file mode 100644 index 000000000..61b6c2538 --- /dev/null +++ b/server/README.md @@ -0,0 +1,3 @@ +# Immich server project + +This project uses the [NestJS](https://nestjs.com/) web framework. Please refer to [the NestJS docs](https://docs.nestjs.com/) for information on getting started as a contributor to this project. diff --git a/server/bin/immich b/server/bin/immich deleted file mode 100755 index 6b7dc3aa3..000000000 --- a/server/bin/immich +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -node /usr/src/app/node_modules/.bin/immich "$@" diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts index 63d439586..f32d58611 100644 --- a/server/e2e/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto } from '@app/domain'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import request from 'supertest'; export const assetApi = { diff --git a/server/e2e/client/auth-api.ts b/server/e2e/client/auth-api.ts index e89e6d057..a8cfe4660 100644 --- a/server/e2e/client/auth-api.ts +++ b/server/e2e/client/auth-api.ts @@ -1,6 +1,7 @@ -import { LoginResponseDto, UserResponseDto } from '@app/domain'; -import { adminSignupStub, loginResponseStub, loginStub } from '@test'; +import { LoginResponseDto } from 'src/dtos/auth.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; import request from 'supertest'; +import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub'; export const authApi = { adminSignUp: async (server: any) => { diff --git a/server/e2e/client/index.ts b/server/e2e/client/index.ts index b4aa2a141..41418ddcc 100644 --- a/server/e2e/client/index.ts +++ b/server/e2e/client/index.ts @@ -1,6 +1,6 @@ -import { assetApi } from './asset-api'; -import { authApi } from './auth-api'; -import { libraryApi } from './library-api'; +import { assetApi } from 'e2e/client/asset-api'; +import { authApi } from 'e2e/client/auth-api'; +import { libraryApi } from 'e2e/client/library-api'; export const api = { authApi, diff --git a/server/e2e/client/library-api.ts b/server/e2e/client/library-api.ts index 070683eb0..70c8c4c36 100644 --- a/server/e2e/client/library-api.ts +++ b/server/e2e/client/library-api.ts @@ -1,4 +1,4 @@ -import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from '@app/domain'; +import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto'; import request from 'supertest'; export const libraryApi = { diff --git a/server/e2e/jobs/jest-e2e.json b/server/e2e/jobs/jest-e2e.json index 333174c5a..b7e62d5f4 100644 --- a/server/e2e/jobs/jest-e2e.json +++ b/server/e2e/jobs/jest-e2e.json @@ -12,13 +12,11 @@ "collectCoverageFrom": [ "/src/**/*.(t|j)s", "!/src/**/*.spec.(t|s)s", - "!/src/infra/migrations/**" + "!/src/migrations/**" ], "coverageDirectory": "./coverage", "moduleNameMapper": { - "^@test(|/.*)$": "/test/$1", - "^@app/immich(|/.*)$": "/src/immich/$1", - "^@app/infra(|/.*)$": "/src/infra/$1", - "^@app/domain(|/.*)$": "/src/domain/$1" + "^test(|/.*)$": "/test/$1", + "^src(|/.*)$": "/src/$1" } } diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts index 5f05d736b..20a9d3202 100644 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts @@ -1,15 +1,19 @@ -import { LibraryResponseDto, LibraryService, LoginResponseDto, StorageEventType } from '@app/domain'; -import { AssetType, LibraryType } from '@app/infra/entities'; +import { api } from 'e2e/client'; import fs from 'node:fs/promises'; import path from 'node:path'; +import { LoginResponseDto } from 'src/dtos/auth.dto'; +import { LibraryResponseDto } from 'src/dtos/library.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { LibraryType } from 'src/entities/library.entity'; +import { StorageEventType } from 'src/interfaces/storage.interface'; +import { LibraryService } from 'src/services/library.service'; import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp, waitForEvent, -} from '../../../src/test-utils/utils'; -import { api } from '../../client'; +} from 'test/utils'; describe(`Library watcher (e2e)`, () => { let server: any; diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index a4ee4977a..3ae27e631 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -1,17 +1,13 @@ -import { LoginResponseDto } from '@app/domain'; -import { LibraryController } from '@app/immich'; -import { LibraryType } from '@app/infra/entities'; -import { errorStub, uuidStub } from '@test/fixtures'; -import * as fs from 'node:fs'; +import { api } from 'e2e/client'; +import fs from 'node:fs'; +import { LibraryController } from 'src/controllers/library.controller'; +import { LoginResponseDto } from 'src/dtos/auth.dto'; +import { LibraryType } from 'src/entities/library.entity'; import request from 'supertest'; +import { errorStub } from 'test/fixtures/error.stub'; +import { uuidStub } from 'test/fixtures/uuid.stub'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from 'test/utils'; import { utimes } from 'utimes'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - restoreTempFolder, - testApp, -} from '../../../src/test-utils/utils'; -import { api } from '../../client'; describe(`${LibraryController.name} (e2e)`, () => { let server: any; @@ -61,11 +57,11 @@ describe(`${LibraryController.name} (e2e)`, () => { expect.arrayContaining([ expect.objectContaining({ isOffline: true, - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', }), expect.objectContaining({ isOffline: true, - originalFileName: 'tanners_ridge', + originalFileName: 'tanners_ridge.jpg', }), ]), ); @@ -97,10 +93,10 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', }), expect.objectContaining({ - originalFileName: 'silver_fir', + originalFileName: 'silver_fir.jpg', }), ]), ); @@ -137,7 +133,7 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(assets[0]).toEqual( expect.objectContaining({ - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-09-25T08:33:30.880Z', exifImageHeight: 534, @@ -184,7 +180,7 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(assets[0]).toEqual( expect.objectContaining({ - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', exifInfo: expect.objectContaining({ dateTimeOriginal: '2012-08-05T11:39:59.000Z', }), @@ -224,7 +220,7 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(assets[0]).toEqual( expect.objectContaining({ - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', exifInfo: expect.objectContaining({ exifImageHeight: 534, exifImageWidth: 800, diff --git a/server/package-lock.json b/server/package-lock.json index eecc75bb2..5e11c3411 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,16 +1,15 @@ { "name": "immich", - "version": "1.98.2", + "version": "1.100.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -22,7 +21,7 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.42.0", + "@opentelemetry/auto-instrumentations-node": "^0.43.0", "@opentelemetry/exporter-prometheus": "^0.49.0", "@opentelemetry/sdk-node": "^0.49.0", "@socket.io/postgres-adapter": "^0.3.1", @@ -82,7 +81,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/sharp": "^0.31.1", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -885,9 +883,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -998,9 +996,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", + "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1289,9 +1287,9 @@ "dev": true }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", - "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", + "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", "cpu": [ "arm64" ], @@ -1310,13 +1308,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.1" + "@img/sharp-libvips-darwin-arm64": "1.0.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", - "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", + "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", "cpu": [ "x64" ], @@ -1335,13 +1333,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.1" + "@img/sharp-libvips-darwin-x64": "1.0.2" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", "cpu": [ "arm64" ], @@ -1360,9 +1358,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", - "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", "cpu": [ "x64" ], @@ -1381,9 +1379,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", - "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", "cpu": [ "arm" ], @@ -1402,9 +1400,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", - "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", "cpu": [ "arm64" ], @@ -1423,9 +1421,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", - "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", "cpu": [ "s390x" ], @@ -1444,9 +1442,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", - "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", "cpu": [ "x64" ], @@ -1465,9 +1463,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", - "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", "cpu": [ "arm64" ], @@ -1486,9 +1484,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", - "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", "cpu": [ "x64" ], @@ -1507,9 +1505,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", - "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", + "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", "cpu": [ "arm" ], @@ -1528,13 +1526,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.1" + "@img/sharp-libvips-linux-arm": "1.0.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", - "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", + "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", "cpu": [ "arm64" ], @@ -1553,13 +1551,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.1" + "@img/sharp-libvips-linux-arm64": "1.0.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", - "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", + "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", "cpu": [ "s390x" ], @@ -1578,13 +1576,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.1" + "@img/sharp-libvips-linux-s390x": "1.0.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", - "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", + "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", "cpu": [ "x64" ], @@ -1603,13 +1601,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.1" + "@img/sharp-libvips-linux-x64": "1.0.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", - "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", + "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", "cpu": [ "arm64" ], @@ -1628,13 +1626,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", - "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", + "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", "cpu": [ "x64" ], @@ -1653,19 +1651,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", - "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", + "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^0.45.0" + "@emnapi/runtime": "^1.1.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0", @@ -1678,9 +1676,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", - "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", + "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", "cpu": [ "ia32" ], @@ -1699,9 +1697,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", - "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", + "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", "cpu": [ "x64" ], @@ -1719,17 +1717,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@immich/cli": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.7.tgz", - "integrity": "sha512-36bpL7SCnbWuaHwuvVmV0iw2dgxX6umk3DhQ5rThJ6C9vOVZs8WY2zMU0voTATlEPoJkpwcQTOMLKFLTPL5OJw==", - "bin": { - "immich": "dist/index.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -2395,9 +2382,9 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", - "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", + "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", "dependencies": { "tslib": "2.6.2" }, @@ -2407,11 +2394,11 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", - "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", + "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", "dependencies": { - "@nestjs/bull-shared": "^10.1.0", + "@nestjs/bull-shared": "^10.1.1", "tslib": "2.6.2" }, "peerDependencies": { @@ -2551,9 +2538,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.3.tgz", - "integrity": "sha512-LAkTe8/CF0uNWM0ecuDwUNTHCi1lVSITmmR4FQ6Ftz1E7ujQCnJ5pMRzd8JRN14vdBkxZZ8VbVF0BDUKoKNxMQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -2579,11 +2566,11 @@ } }, "node_modules/@nestjs/config": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", - "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.1.tgz", + "integrity": "sha512-tFZyLJKanSAu51ygQ6ZBSpx95pRcwS6qSpJDW6FFgRQzkOaOUXpL8qD8yMNoYoYxuJCxph+waiBaWKgFWxn3sw==", "dependencies": { - "dotenv": "16.4.1", + "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21", "uuid": "9.0.1" @@ -2593,21 +2580,10 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, "node_modules/@nestjs/core": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", - "integrity": "sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2673,13 +2649,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", - "integrity": "sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" }, @@ -2693,11 +2669,11 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.3.tgz", - "integrity": "sha512-QqM9BMTdYPvXOqx3oWrv130HOtc2krPvfgqgDsPWkBLfR+TssrA5QDaTW8HSjEQAfmugvHwhEAAU4+yXRl6tKg==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "dependencies": { - "socket.io": "4.7.4", + "socket.io": "4.7.5", "tslib": "2.6.2" }, "funding": { @@ -2778,9 +2754,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz", - "integrity": "sha512-kX20GfjAImL5grd/i69uD/x7sc00BaqGcP2dRG3ilqshQUuy5DOmspLCr3a2C8xmVU7kzK4spT0oTxhe6WcCAA==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -2820,9 +2796,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.3.tgz", - "integrity": "sha512-cR5cB0bLS87vd0iu7Nud/4x2EH1Vs0aIgwGWd0eH/5SAw0rrDNU81PiOde+rnMXETbxvSVfOZuLRyn7/WQtGUg==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2910,25 +2886,25 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.42.0.tgz", - "integrity": "sha512-fxcB7My5QTVfX6kBH4r5OFduGSxdpROgyIu7CqClp1psFHfVaBMQd4lbK2u+39K5kbjzJT2OaUP8yQuAvKJqBg==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.43.0.tgz", + "integrity": "sha512-2WvHUSi/QVeVG8ObPD0Ls6WevfIbQjspxIQRuHaQFWXhmEwy/MsEcoQUjbNKXwO5516aS04GTydKEoRKsMwhdA==", "dependencies": { "@opentelemetry/instrumentation": "^0.49.1", "@opentelemetry/instrumentation-amqplib": "^0.35.0", "@opentelemetry/instrumentation-aws-lambda": "^0.39.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.39.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.39.1", "@opentelemetry/instrumentation-bunyan": "^0.36.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.36.0", "@opentelemetry/instrumentation-connect": "^0.34.0", "@opentelemetry/instrumentation-cucumber": "^0.4.0", "@opentelemetry/instrumentation-dataloader": "^0.7.0", "@opentelemetry/instrumentation-dns": "^0.34.0", - "@opentelemetry/instrumentation-express": "^0.36.0", + "@opentelemetry/instrumentation-express": "^0.36.1", "@opentelemetry/instrumentation-fastify": "^0.34.0", "@opentelemetry/instrumentation-fs": "^0.10.0", "@opentelemetry/instrumentation-generic-pool": "^0.34.0", - "@opentelemetry/instrumentation-graphql": "^0.38.0", + "@opentelemetry/instrumentation-graphql": "^0.38.1", "@opentelemetry/instrumentation-grpc": "^0.49.1", "@opentelemetry/instrumentation-hapi": "^0.35.0", "@opentelemetry/instrumentation-http": "^0.49.1", @@ -2937,13 +2913,13 @@ "@opentelemetry/instrumentation-koa": "^0.38.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.35.0", "@opentelemetry/instrumentation-memcached": "^0.34.0", - "@opentelemetry/instrumentation-mongodb": "^0.40.0", + "@opentelemetry/instrumentation-mongodb": "^0.41.0", "@opentelemetry/instrumentation-mongoose": "^0.36.0", "@opentelemetry/instrumentation-mysql": "^0.36.0", "@opentelemetry/instrumentation-mysql2": "^0.36.0", "@opentelemetry/instrumentation-nestjs-core": "^0.35.0", "@opentelemetry/instrumentation-net": "^0.34.0", - "@opentelemetry/instrumentation-pg": "^0.39.0", + "@opentelemetry/instrumentation-pg": "^0.39.1", "@opentelemetry/instrumentation-pino": "^0.36.0", "@opentelemetry/instrumentation-redis": "^0.37.0", "@opentelemetry/instrumentation-redis-4": "^0.37.0", @@ -3465,9 +3441,9 @@ } }, "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.40.0.tgz", - "integrity": "sha512-ldlJUW/1UlnGtIWBt7fIUl+7+TGOKxIU+0Js5ukpXfQc07ENYFeck5TdbFjvYtF8GppPErnsZJiFiRdYm6Pv/Q==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.41.0.tgz", + "integrity": "sha512-DlSH0oyEuTW5gprCUppb0Qe3pK3cpUUFW5eTmayWNyICI1LFunwtcrULTNv6UiThD/V5ykAf/GGGEa7KFAmkog==", "dependencies": { "@opentelemetry/instrumentation": "^0.49.1", "@opentelemetry/sdk-metrics": "^1.9.1", @@ -4645,9 +4621,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "dev": true }, "node_modules/@types/luxon": { @@ -4706,9 +4682,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dependencies": { "undici-types": "~5.26.4" } @@ -4838,15 +4814,6 @@ "@types/node": "*" } }, - "node_modules/@types/sharp": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", - "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/shimmer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", @@ -4942,16 +4909,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4960,7 +4927,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4977,19 +4944,19 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5005,16 +4972,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5022,18 +4989,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5049,12 +5016,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5062,13 +5029,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5077,7 +5044,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5114,21 +5081,21 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5139,16 +5106,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -5531,38 +5498,77 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "node_modules/archiver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.0.tgz", - "integrity": "sha512-R9HM9egs8FfktSqUqyjlKmvF4U+CWNqm/2tlROV+lOFg79MLdT67ae1l3hU47pGy8twSXxHoiefMCh43w0BriQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "dependencies": { - "archiver-utils": "^5.0.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", - "zip-stream": "^6.0.0" + "zip-stream": "^6.0.1" }, "engines": { "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.1.tgz", - "integrity": "sha512-MMAoLdMvT/nckofX1tCLrf7uJce4jTNkiT6smA2u57AOImc1nce7mR3EDujxL5yv6/MnILuQH4sAsPtDS8kTvg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { "node": ">= 14" } }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/archiver/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -6587,12 +6593,13 @@ "dev": true }, "node_modules/compress-commons": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.1.tgz", - "integrity": "sha512-l7occIJn8YwlCEbWUCrG6gPms9qnJTCZSaznCa5HaV+yJMH4kM8BDc7q9NyoQuoiB2O6jKgTcTeY462qw6MyHw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" }, @@ -7032,9 +7039,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -7756,16 +7763,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -7796,33 +7803,10 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -7845,20 +7829,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8370,9 +8340,9 @@ } }, "node_modules/geo-tz": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.1.tgz", - "integrity": "sha512-hpFbw3NKFOVy461NrWIt6Z6JQpGnMpYvNpvDunIrixbHsBPOnDcrfao0p+o/7gsMJnkhSYnTJ9DkyV2tXBLI8w==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz", + "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==", "dependencies": { "@turf/boolean-point-in-polygon": "^6.5.0", "@turf/helpers": "^6.5.0", @@ -8768,9 +8738,9 @@ } }, "node_modules/i18n-iso-countries": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.1.tgz", - "integrity": "sha512-9DXmAMkfGcGNE+E/2fE85UUjjkPeT0LHMA8d+kcxXiO+s50W28lxiICel8f8qWZmCNic1cuhN1+nw7ZazMQJFA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.0.tgz", + "integrity": "sha512-MP2+aAJwvBTuruaMEj+mPEhu4D9rn03GkkbuP8/xkvNzhVwNe2cAg1ivkL5Oj+vwqEwvIcf5C7Q+5Y/UZNLBHw==", "dependencies": { "diacritics": "1.3.0" }, @@ -11264,9 +11234,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "engines": { "node": ">=12" }, @@ -12255,9 +12225,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -12409,42 +12379,42 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/sharp": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", - "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", + "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" + "detect-libc": "^2.0.3", + "semver": "^7.6.0" }, "engines": { - "libvips": ">=8.15.1", + "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.2", - "@img/sharp-darwin-x64": "0.33.2", - "@img/sharp-libvips-darwin-arm64": "1.0.1", - "@img/sharp-libvips-darwin-x64": "1.0.1", - "@img/sharp-libvips-linux-arm": "1.0.1", - "@img/sharp-libvips-linux-arm64": "1.0.1", - "@img/sharp-libvips-linux-s390x": "1.0.1", - "@img/sharp-libvips-linux-x64": "1.0.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", - "@img/sharp-libvips-linuxmusl-x64": "1.0.1", - "@img/sharp-linux-arm": "0.33.2", - "@img/sharp-linux-arm64": "0.33.2", - "@img/sharp-linux-s390x": "0.33.2", - "@img/sharp-linux-x64": "0.33.2", - "@img/sharp-linuxmusl-arm64": "0.33.2", - "@img/sharp-linuxmusl-x64": "0.33.2", - "@img/sharp-wasm32": "0.33.2", - "@img/sharp-win32-ia32": "0.33.2", - "@img/sharp-win32-x64": "0.33.2" + "@img/sharp-darwin-arm64": "0.33.3", + "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.3", + "@img/sharp-linux-arm64": "0.33.3", + "@img/sharp-linux-s390x": "0.33.3", + "@img/sharp-linux-x64": "0.33.3", + "@img/sharp-linuxmusl-arm64": "0.33.3", + "@img/sharp-linuxmusl-x64": "0.33.3", + "@img/sharp-wasm32": "0.33.3", + "@img/sharp-win32-ia32": "0.33.3", + "@img/sharp-win32-x64": "0.33.3" } }, "node_modules/shebang-command": { @@ -12579,9 +12549,9 @@ "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==" }, "node_modules/socket.io": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", - "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -12696,9 +12666,9 @@ "dev": true }, "node_modules/sql-formatter": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.2.0.tgz", - "integrity": "sha512-k1gDOblvmtzmrBT687Y167ElwQI/8KrlhfKeIUXsi6jw7Rp5n3G8TkMFZF0Z9NG7rAzHKXUlJ8kfmcIfMf5lFg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.0.tgz", + "integrity": "sha512-1aDYVEX+dwOSCkRYns4HEGupRZoaivcsNpU4IzR+MVC+cWFYK9/dce7pr4aId4+ED2iK9PNs3j1Vdf8C+SIvDg==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -13949,9 +13919,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -14487,12 +14457,12 @@ } }, "node_modules/zip-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.0.tgz", - "integrity": "sha512-X0WFquRRDtL9HR9hc1OrabOP/VKJEX7gAr2geayt3b7dLgXgSXI6ucC4CphLQP/aQt2GyHIYgmXxtC+dVdghAQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "dependencies": { "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.0", + "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" }, "engines": { @@ -15098,9 +15068,9 @@ } }, "@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -15194,9 +15164,9 @@ } }, "@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", + "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", "optional": true, "requires": { "tslib": "^2.4.0" @@ -15426,151 +15396,146 @@ "dev": true }, "@img/sharp-darwin-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", - "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", + "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-arm64": "1.0.1" + "@img/sharp-libvips-darwin-arm64": "1.0.2" } }, "@img/sharp-darwin-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", - "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", + "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-x64": "1.0.1" + "@img/sharp-libvips-darwin-x64": "1.0.2" } }, "@img/sharp-libvips-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", "optional": true }, "@img/sharp-libvips-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", - "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", "optional": true }, "@img/sharp-libvips-linux-arm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", - "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", "optional": true }, "@img/sharp-libvips-linux-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", - "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", "optional": true }, "@img/sharp-libvips-linux-s390x": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", - "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", "optional": true }, "@img/sharp-libvips-linux-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", - "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", - "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", - "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", "optional": true }, "@img/sharp-linux-arm": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", - "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", + "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm": "1.0.1" + "@img/sharp-libvips-linux-arm": "1.0.2" } }, "@img/sharp-linux-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", - "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", + "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm64": "1.0.1" + "@img/sharp-libvips-linux-arm64": "1.0.2" } }, "@img/sharp-linux-s390x": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", - "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", + "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-s390x": "1.0.1" + "@img/sharp-libvips-linux-s390x": "1.0.2" } }, "@img/sharp-linux-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", - "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", + "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", "optional": true, "requires": { - "@img/sharp-libvips-linux-x64": "1.0.1" + "@img/sharp-libvips-linux-x64": "1.0.2" } }, "@img/sharp-linuxmusl-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", - "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", + "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" } }, "@img/sharp-linuxmusl-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", - "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", + "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" } }, "@img/sharp-wasm32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", - "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", + "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", "optional": true, "requires": { - "@emnapi/runtime": "^0.45.0" + "@emnapi/runtime": "^1.1.0" } }, "@img/sharp-win32-ia32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", - "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", + "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", "optional": true }, "@img/sharp-win32-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", - "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", + "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", "optional": true }, - "@immich/cli": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.7.tgz", - "integrity": "sha512-36bpL7SCnbWuaHwuvVmV0iw2dgxX6umk3DhQ5rThJ6C9vOVZs8WY2zMU0voTATlEPoJkpwcQTOMLKFLTPL5OJw==" - }, "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -16077,19 +16042,19 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", - "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", + "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", "requires": { "tslib": "2.6.2" } }, "@nestjs/bullmq": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", - "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", + "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", "requires": { - "@nestjs/bull-shared": "^10.1.0", + "@nestjs/bull-shared": "^10.1.1", "tslib": "2.6.2" } }, @@ -16179,9 +16144,9 @@ } }, "@nestjs/common": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.3.tgz", - "integrity": "sha512-LAkTe8/CF0uNWM0ecuDwUNTHCi1lVSITmmR4FQ6Ftz1E7ujQCnJ5pMRzd8JRN14vdBkxZZ8VbVF0BDUKoKNxMQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "requires": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -16189,27 +16154,20 @@ } }, "@nestjs/config": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", - "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.1.tgz", + "integrity": "sha512-tFZyLJKanSAu51ygQ6ZBSpx95pRcwS6qSpJDW6FFgRQzkOaOUXpL8qD8yMNoYoYxuJCxph+waiBaWKgFWxn3sw==", "requires": { - "dotenv": "16.4.1", + "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21", "uuid": "9.0.1" - }, - "dependencies": { - "dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==" - } } }, "@nestjs/core": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", - "integrity": "sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -16234,23 +16192,23 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", - "integrity": "sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" } }, "@nestjs/platform-socket.io": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.3.tgz", - "integrity": "sha512-QqM9BMTdYPvXOqx3oWrv130HOtc2krPvfgqgDsPWkBLfR+TssrA5QDaTW8HSjEQAfmugvHwhEAAU4+yXRl6tKg==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "requires": { - "socket.io": "4.7.4", + "socket.io": "4.7.5", "tslib": "2.6.2" } }, @@ -16298,9 +16256,9 @@ } }, "@nestjs/testing": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz", - "integrity": "sha512-kX20GfjAImL5grd/i69uD/x7sc00BaqGcP2dRG3ilqshQUuy5DOmspLCr3a2C8xmVU7kzK4spT0oTxhe6WcCAA==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "requires": { "tslib": "2.6.2" @@ -16315,9 +16273,9 @@ } }, "@nestjs/websockets": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.3.tgz", - "integrity": "sha512-cR5cB0bLS87vd0iu7Nud/4x2EH1Vs0aIgwGWd0eH/5SAw0rrDNU81PiOde+rnMXETbxvSVfOZuLRyn7/WQtGUg==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -16371,25 +16329,25 @@ } }, "@opentelemetry/auto-instrumentations-node": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.42.0.tgz", - "integrity": "sha512-fxcB7My5QTVfX6kBH4r5OFduGSxdpROgyIu7CqClp1psFHfVaBMQd4lbK2u+39K5kbjzJT2OaUP8yQuAvKJqBg==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.43.0.tgz", + "integrity": "sha512-2WvHUSi/QVeVG8ObPD0Ls6WevfIbQjspxIQRuHaQFWXhmEwy/MsEcoQUjbNKXwO5516aS04GTydKEoRKsMwhdA==", "requires": { "@opentelemetry/instrumentation": "^0.49.1", "@opentelemetry/instrumentation-amqplib": "^0.35.0", "@opentelemetry/instrumentation-aws-lambda": "^0.39.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.39.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.39.1", "@opentelemetry/instrumentation-bunyan": "^0.36.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.36.0", "@opentelemetry/instrumentation-connect": "^0.34.0", "@opentelemetry/instrumentation-cucumber": "^0.4.0", "@opentelemetry/instrumentation-dataloader": "^0.7.0", "@opentelemetry/instrumentation-dns": "^0.34.0", - "@opentelemetry/instrumentation-express": "^0.36.0", + "@opentelemetry/instrumentation-express": "^0.36.1", "@opentelemetry/instrumentation-fastify": "^0.34.0", "@opentelemetry/instrumentation-fs": "^0.10.0", "@opentelemetry/instrumentation-generic-pool": "^0.34.0", - "@opentelemetry/instrumentation-graphql": "^0.38.0", + "@opentelemetry/instrumentation-graphql": "^0.38.1", "@opentelemetry/instrumentation-grpc": "^0.49.1", "@opentelemetry/instrumentation-hapi": "^0.35.0", "@opentelemetry/instrumentation-http": "^0.49.1", @@ -16398,13 +16356,13 @@ "@opentelemetry/instrumentation-koa": "^0.38.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.35.0", "@opentelemetry/instrumentation-memcached": "^0.34.0", - "@opentelemetry/instrumentation-mongodb": "^0.40.0", + "@opentelemetry/instrumentation-mongodb": "^0.41.0", "@opentelemetry/instrumentation-mongoose": "^0.36.0", "@opentelemetry/instrumentation-mysql": "^0.36.0", "@opentelemetry/instrumentation-mysql2": "^0.36.0", "@opentelemetry/instrumentation-nestjs-core": "^0.35.0", "@opentelemetry/instrumentation-net": "^0.34.0", - "@opentelemetry/instrumentation-pg": "^0.39.0", + "@opentelemetry/instrumentation-pg": "^0.39.1", "@opentelemetry/instrumentation-pino": "^0.36.0", "@opentelemetry/instrumentation-redis": "^0.37.0", "@opentelemetry/instrumentation-redis-4": "^0.37.0", @@ -16735,9 +16693,9 @@ } }, "@opentelemetry/instrumentation-mongodb": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.40.0.tgz", - "integrity": "sha512-ldlJUW/1UlnGtIWBt7fIUl+7+TGOKxIU+0Js5ukpXfQc07ENYFeck5TdbFjvYtF8GppPErnsZJiFiRdYm6Pv/Q==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.41.0.tgz", + "integrity": "sha512-DlSH0oyEuTW5gprCUppb0Qe3pK3cpUUFW5eTmayWNyICI1LFunwtcrULTNv6UiThD/V5ykAf/GGGEa7KFAmkog==", "requires": { "@opentelemetry/instrumentation": "^0.49.1", "@opentelemetry/sdk-metrics": "^1.9.1", @@ -17682,9 +17640,9 @@ } }, "@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "dev": true }, "@types/luxon": { @@ -17743,9 +17701,9 @@ } }, "@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "requires": { "undici-types": "~5.26.4" } @@ -17862,15 +17820,6 @@ "@types/node": "*" } }, - "@types/sharp": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", - "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/shimmer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", @@ -17966,16 +17915,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -17985,54 +17934,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" } }, "@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -18062,27 +18011,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -18396,17 +18345,17 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "archiver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.0.tgz", - "integrity": "sha512-R9HM9egs8FfktSqUqyjlKmvF4U+CWNqm/2tlROV+lOFg79MLdT67ae1l3hU47pGy8twSXxHoiefMCh43w0BriQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "requires": { - "archiver-utils": "^5.0.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", - "zip-stream": "^6.0.0" + "zip-stream": "^6.0.1" }, "dependencies": { "buffer": { @@ -18438,16 +18387,40 @@ } }, "archiver-utils": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.1.tgz", - "integrity": "sha512-MMAoLdMvT/nckofX1tCLrf7uJce4jTNkiT6smA2u57AOImc1nce7mR3EDujxL5yv6/MnILuQH4sAsPtDS8kTvg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "requires": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } } }, "are-we-there-yet": { @@ -19178,12 +19151,13 @@ "dev": true }, "compress-commons": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.1.tgz", - "integrity": "sha512-l7occIJn8YwlCEbWUCrG6gPms9qnJTCZSaznCa5HaV+yJMH4kM8BDc7q9NyoQuoiB2O6jKgTcTeY462qw6MyHw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "requires": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" }, @@ -19489,9 +19463,9 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, "detect-newline": { "version": "3.1.0", @@ -20023,16 +19997,16 @@ } }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -20060,29 +20034,10 @@ "vary": "~1.1.2" }, "dependencies": { - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "debug": { "version": "2.6.9", @@ -20101,17 +20056,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } } } }, @@ -20526,9 +20470,9 @@ "dev": true }, "geo-tz": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.1.tgz", - "integrity": "sha512-hpFbw3NKFOVy461NrWIt6Z6JQpGnMpYvNpvDunIrixbHsBPOnDcrfao0p+o/7gsMJnkhSYnTJ9DkyV2tXBLI8w==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz", + "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==", "requires": { "@turf/boolean-point-in-polygon": "^6.5.0", "@turf/helpers": "^6.5.0", @@ -20798,9 +20742,9 @@ "dev": true }, "i18n-iso-countries": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.1.tgz", - "integrity": "sha512-9DXmAMkfGcGNE+E/2fE85UUjjkPeT0LHMA8d+kcxXiO+s50W28lxiICel8f8qWZmCNic1cuhN1+nw7ZazMQJFA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.0.tgz", + "integrity": "sha512-MP2+aAJwvBTuruaMEj+mPEhu4D9rn03GkkbuP8/xkvNzhVwNe2cAg1ivkL5Oj+vwqEwvIcf5C7Q+5Y/UZNLBHw==", "requires": { "diacritics": "1.3.0" } @@ -22710,9 +22654,9 @@ "dev": true }, "picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" }, "pirates": { "version": "4.0.6", @@ -23415,9 +23359,9 @@ } }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "requires": { "lru-cache": "^6.0.0" }, @@ -23552,32 +23496,32 @@ } }, "sharp": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", - "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", + "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", "requires": { - "@img/sharp-darwin-arm64": "0.33.2", - "@img/sharp-darwin-x64": "0.33.2", - "@img/sharp-libvips-darwin-arm64": "1.0.1", - "@img/sharp-libvips-darwin-x64": "1.0.1", - "@img/sharp-libvips-linux-arm": "1.0.1", - "@img/sharp-libvips-linux-arm64": "1.0.1", - "@img/sharp-libvips-linux-s390x": "1.0.1", - "@img/sharp-libvips-linux-x64": "1.0.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", - "@img/sharp-libvips-linuxmusl-x64": "1.0.1", - "@img/sharp-linux-arm": "0.33.2", - "@img/sharp-linux-arm64": "0.33.2", - "@img/sharp-linux-s390x": "0.33.2", - "@img/sharp-linux-x64": "0.33.2", - "@img/sharp-linuxmusl-arm64": "0.33.2", - "@img/sharp-linuxmusl-x64": "0.33.2", - "@img/sharp-wasm32": "0.33.2", - "@img/sharp-win32-ia32": "0.33.2", - "@img/sharp-win32-x64": "0.33.2", + "@img/sharp-darwin-arm64": "0.33.3", + "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.3", + "@img/sharp-linux-arm64": "0.33.3", + "@img/sharp-linux-s390x": "0.33.3", + "@img/sharp-linux-x64": "0.33.3", + "@img/sharp-linuxmusl-arm64": "0.33.3", + "@img/sharp-linuxmusl-x64": "0.33.3", + "@img/sharp-wasm32": "0.33.3", + "@img/sharp-win32-ia32": "0.33.3", + "@img/sharp-win32-x64": "0.33.3", "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" + "detect-libc": "^2.0.3", + "semver": "^7.6.0" } }, "shebang-command": { @@ -23683,9 +23627,9 @@ "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==" }, "socket.io": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", - "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -23787,9 +23731,9 @@ "dev": true }, "sql-formatter": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.2.0.tgz", - "integrity": "sha512-k1gDOblvmtzmrBT687Y167ElwQI/8KrlhfKeIUXsi6jw7Rp5n3G8TkMFZF0Z9NG7rAzHKXUlJ8kfmcIfMf5lFg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.0.tgz", + "integrity": "sha512-1aDYVEX+dwOSCkRYns4HEGupRZoaivcsNpU4IzR+MVC+cWFYK9/dce7pr4aId4+ED2iK9PNs3j1Vdf8C+SIvDg==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -24642,9 +24586,9 @@ } }, "typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "devOptional": true }, "ua-parser-js": { @@ -25018,12 +24962,12 @@ "dev": true }, "zip-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.0.tgz", - "integrity": "sha512-X0WFquRRDtL9HR9hc1OrabOP/VKJEX7gAr2geayt3b7dLgXgSXI6ucC4CphLQP/aQt2GyHIYgmXxtC+dVdghAQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "requires": { "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.0", + "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" }, "dependencies": { diff --git a/server/package.json b/server/package.json index d60f67a1f..4e3915682 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.98.2", + "version": "1.100.0", "description": "", "author": "", "private": true, @@ -25,16 +25,15 @@ "e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand", "typeorm": "typeorm", "typeorm:migrations:create": "typeorm migration:create", - "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", - "typeorm:migrations:run": "typeorm migration:run -d ./dist/infra/database.config.js", - "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/infra/database.config.js", - "typeorm:schema:drop": "typeorm query -d ./dist/infra/database.config.js 'DROP schema public cascade; CREATE schema public;'", + "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js", + "typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js", + "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js", + "typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'", "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run", - "sql:generate": "node ./dist/infra/sql-generator/" + "sql:generate": "node ./dist/utils/sql.js" }, "dependencies": { "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.7", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -46,7 +45,7 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.42.0", + "@opentelemetry/auto-instrumentations-node": "^0.43.0", "@opentelemetry/exporter-prometheus": "^0.49.0", "@opentelemetry/sdk-node": "^0.49.0", "@socket.io/postgres-adapter": "^0.3.1", @@ -106,7 +105,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/sharp": "^0.31.1", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -146,29 +144,30 @@ "^.+\\.ts$": "ts-jest" }, "collectCoverageFrom": [ - "/src/**/*.(t|j)s", - "!/src/infra/**/*", - "!/src/immich/controllers/**/*" + "/src/cores/*.(t|j)s", + "/src/dtos/*.(t|j)s", + "/src/interfaces/*.(t|j)s", + "/src/services/*.(t|j)s", + "/src/utils/*.(t|j)s", + "/src/*.t|j)s" ], "coverageDirectory": "./coverage", "coverageThreshold": { - "./src/domain/": { - "branches": 75, - "functions": 80, - "lines": 90, - "statements": 90 + "./src/": { + "branches": 70, + "functions": 75, + "lines": 80, + "statements": 80 } }, "testEnvironment": "node", "moduleNameMapper": { - "^@test(|/.*)$": "/test/$1", - "^@app/immich(|/.*)$": "/src/immich/$1", - "^@app/infra(|/.*)$": "/src/infra/$1", - "^@app/domain(|/.*)$": "/src/domain/$1" + "^test(|/.*)$": "/test/$1", + "^src(|/.*)$": "/src/$1" }, "globalSetup": "/test/global-setup.js" }, "volta": { - "node": "20.11.1" + "node": "20.12.0" } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts new file mode 100644 index 000000000..ded08a96a --- /dev/null +++ b/server/src/app.module.ts @@ -0,0 +1,86 @@ +import { BullModule } from '@nestjs/bullmq'; +import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { OpenTelemetryModule } from 'nestjs-otel'; +import { commands } from 'src/commands'; +import { bullConfig, bullQueues, immichAppConfig } from 'src/config'; +import { controllers } from 'src/controllers'; +import { databaseConfig } from 'src/database.config'; +import { entities } from 'src/entities'; +import { AuthGuard } from 'src/middleware/auth.guard'; +import { ErrorInterceptor } from 'src/middleware/error.interceptor'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; +import { repositories } from 'src/repositories'; +import { services } from 'src/services'; +import { ApiService } from 'src/services/api.service'; +import { MicroservicesService } from 'src/services/microservices.service'; +import { otelConfig } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; + +const providers = [ImmichLogger]; +const common = [...services, ...providers, ...repositories]; + +const middleware = [ + FileUploadInterceptor, + { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, + { provide: APP_GUARD, useClass: AuthGuard }, +]; + +const imports = [ + BullModule.forRoot(bullConfig), + BullModule.registerQueue(...bullQueues), + ConfigModule.forRoot(immichAppConfig), + EventEmitterModule.forRoot(), + OpenTelemetryModule.forRoot(otelConfig), + TypeOrmModule.forRoot(databaseConfig), + TypeOrmModule.forFeature(entities), +]; + +@Module({ + imports: [...imports, ScheduleModule.forRoot()], + controllers: [...controllers], + providers: [...common, ...middleware], +}) +export class ApiModule implements OnModuleInit { + constructor(private service: ApiService) {} + + async onModuleInit() { + await this.service.init(); + } +} + +@Module({ + imports: [...imports], + providers: [...common, SchedulerRegistry], +}) +export class MicroservicesModule implements OnModuleInit { + constructor(private service: MicroservicesService) {} + + async onModuleInit() { + await this.service.init(); + } +} + +@Module({ + imports: [...imports], + providers: [...common, ...commands, SchedulerRegistry], +}) +export class ImmichAdminModule {} + +@Module({ + imports: [ + ConfigModule.forRoot(immichAppConfig), + EventEmitterModule.forRoot(), + TypeOrmModule.forRoot(databaseConfig), + TypeOrmModule.forFeature(entities), + OpenTelemetryModule.forRoot(otelConfig), + ], + controllers: [...controllers], + providers: [...common, ...middleware, SchedulerRegistry], +}) +export class AppTestModule {} diff --git a/server/src/commands/index.ts b/server/src/commands/index.ts new file mode 100644 index 000000000..016a26cb3 --- /dev/null +++ b/server/src/commands/index.ts @@ -0,0 +1,14 @@ +import { ListUsersCommand } from 'src/commands/list-users.command'; +import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; +import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; +import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; + +export const commands = [ + ResetAdminPasswordCommand, + PromptPasswordQuestions, + EnablePasswordLoginCommand, + DisablePasswordLoginCommand, + EnableOAuthLogin, + DisableOAuthLogin, + ListUsersCommand, +]; diff --git a/server/src/immich-admin/commands/list-users.command.ts b/server/src/commands/list-users.command.ts similarity index 55% rename from server/src/immich-admin/commands/list-users.command.ts rename to server/src/commands/list-users.command.ts index 15ab0a240..32bcc35d9 100644 --- a/server/src/immich-admin/commands/list-users.command.ts +++ b/server/src/commands/list-users.command.ts @@ -1,6 +1,6 @@ -import { UserService } from '@app/domain'; import { Command, CommandRunner } from 'nest-commander'; -import { CLI_USER } from '../constants'; +import { UserEntity } from 'src/entities/user.entity'; +import { UserService } from 'src/services/user.service'; @Command({ name: 'list-users', @@ -13,7 +13,16 @@ export class ListUsersCommand extends CommandRunner { async run(): Promise { try { - const users = await this.userService.getAll(CLI_USER, true); + const users = await this.userService.getAll( + { + user: { + id: 'cli', + email: 'cli@immich.app', + isAdmin: true, + } as UserEntity, + }, + true, + ); console.dir(users); } catch (error) { console.error(error); diff --git a/server/src/immich-admin/commands/oauth-login.ts b/server/src/commands/oauth-login.ts similarity index 92% rename from server/src/immich-admin/commands/oauth-login.ts rename to server/src/commands/oauth-login.ts index 23747bf03..c9bb4d5ef 100644 --- a/server/src/immich-admin/commands/oauth-login.ts +++ b/server/src/commands/oauth-login.ts @@ -1,5 +1,5 @@ -import { SystemConfigService } from '@app/domain'; import { Command, CommandRunner } from 'nest-commander'; +import { SystemConfigService } from 'src/services/system-config.service'; @Command({ name: 'enable-oauth-login', diff --git a/server/src/immich-admin/commands/password-login.ts b/server/src/commands/password-login.ts similarity index 93% rename from server/src/immich-admin/commands/password-login.ts rename to server/src/commands/password-login.ts index e6eea2c72..3d992f858 100644 --- a/server/src/immich-admin/commands/password-login.ts +++ b/server/src/commands/password-login.ts @@ -1,5 +1,5 @@ -import { SystemConfigService } from '@app/domain'; import { Command, CommandRunner } from 'nest-commander'; +import { SystemConfigService } from 'src/services/system-config.service'; @Command({ name: 'enable-password-login', diff --git a/server/src/immich-admin/commands/reset-admin-password.command.ts b/server/src/commands/reset-admin-password.command.ts similarity index 92% rename from server/src/immich-admin/commands/reset-admin-password.command.ts rename to server/src/commands/reset-admin-password.command.ts index d19ddf433..a186603a3 100644 --- a/server/src/immich-admin/commands/reset-admin-password.command.ts +++ b/server/src/commands/reset-admin-password.command.ts @@ -1,5 +1,6 @@ -import { UserResponseDto, UserService } from '@app/domain'; import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; +import { UserResponseDto } from 'src/dtos/user.dto'; +import { UserService } from 'src/services/user.service'; @Command({ name: 'reset-admin-password', diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 000000000..c7d2302c1 --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,71 @@ +import { RegisterQueueOptions } from '@nestjs/bullmq'; +import { ConfigModuleOptions } from '@nestjs/config'; +import { QueueOptions } from 'bullmq'; +import { RedisOptions } from 'ioredis'; +import Joi from 'joi'; +import { LogLevel } from 'src/entities/system-config.entity'; +import { QueueName } from 'src/interfaces/job.interface'; + +const WHEN_DB_URL_SET = Joi.when('DB_URL', { + is: Joi.exist(), + then: Joi.string().optional(), + otherwise: Joi.string().required(), +}); + +export const immichAppConfig: ConfigModuleOptions = { + envFilePath: '.env', + isGlobal: true, + validationSchema: Joi.object({ + NODE_ENV: Joi.string().optional().valid('development', 'production', 'staging').default('development'), + LOG_LEVEL: Joi.string() + .optional() + .valid(...Object.values(LogLevel)), + + DB_USERNAME: WHEN_DB_URL_SET, + DB_PASSWORD: WHEN_DB_URL_SET, + DB_DATABASE_NAME: WHEN_DB_URL_SET, + DB_URL: Joi.string().optional(), + DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'), + + MACHINE_LEARNING_PORT: Joi.number().optional(), + MICROSERVICES_PORT: Joi.number().optional(), + IMMICH_METRICS_PORT: Joi.number().optional(), + + IMMICH_METRICS: Joi.boolean().optional().default(false), + IMMICH_HOST_METRICS: Joi.boolean().optional().default(false), + IMMICH_API_METRICS: Joi.boolean().optional().default(false), + IMMICH_IO_METRICS: Joi.boolean().optional().default(false), + }), +}; + +function parseRedisConfig(): RedisOptions { + const redisUrl = process.env.REDIS_URL; + if (redisUrl && redisUrl.startsWith('ioredis://')) { + try { + const decodedString = Buffer.from(redisUrl.slice(10), 'base64').toString(); + return JSON.parse(decodedString); + } catch (error) { + throw new Error(`Failed to decode redis options: ${error}`); + } + } + return { + host: process.env.REDIS_HOSTNAME || 'immich_redis', + port: Number.parseInt(process.env.REDIS_PORT || '6379'), + db: Number.parseInt(process.env.REDIS_DBINDEX || '0'), + username: process.env.REDIS_USERNAME || undefined, + password: process.env.REDIS_PASSWORD || undefined, + path: process.env.REDIS_SOCKET || undefined, + }; +} + +export const bullConfig: QueueOptions = { + prefix: 'immich_bull', + connection: parseRedisConfig(), + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, +}; + +export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name })); diff --git a/server/src/constants.ts b/server/src/constants.ts new file mode 100644 index 000000000..1289701dd --- /dev/null +++ b/server/src/constants.ts @@ -0,0 +1,109 @@ +import { Duration } from 'luxon'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { Version } from 'src/utils/version'; + +export const SALT_ROUNDS = 10; + +const { version } = JSON.parse(readFileSync('./package.json', 'utf8')); +export const serverVersion = Version.fromString(version); + +export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); +export const ONE_HOUR = Duration.fromObject({ hours: 1 }); + +export const envName = (process.env.NODE_ENV || 'development').toUpperCase(); +export const isDev = process.env.NODE_ENV === 'development'; +export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; +export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www'; + +const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources'; + +export const citiesFile = 'cities500.txt'; +export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt'); +export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt'); +export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt'); +export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); + +export const MOBILE_REDIRECT = 'app.immich:/'; +export const LOGIN_URL = '/auth/login?autoLaunch=0'; +export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; +export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; +export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; +export const IMMICH_API_KEY_NAME = 'api_key'; +export const IMMICH_API_KEY_HEADER = 'x-api-key'; +export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; +export enum AuthType { + PASSWORD = 'password', + OAUTH = 'oauth', +} + +export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico']; + +export const FACE_THUMBNAIL_SIZE = 250; + +export const supportedYearTokens = ['y', 'yy']; +export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM']; +export const supportedWeekTokens = ['W', 'WW']; +export const supportedDayTokens = ['d', 'dd']; +export const supportedHourTokens = ['h', 'hh', 'H', 'HH']; +export const supportedMinuteTokens = ['m', 'mm']; +export const supportedSecondTokens = ['s', 'ss', 'SSS']; +export const supportedPresetTokens = [ + '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', + '{{y}}/{{MM}}-{{dd}}/{{filename}}', + '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', + '{{y}}/{{MM}}/{{filename}}', + '{{y}}/{{MMM}}/{{filename}}', + '{{y}}/{{MMMM}}/{{filename}}', + '{{y}}/{{MM}}/{{dd}}/{{filename}}', + '{{y}}/{{MMMM}}/{{dd}}/{{filename}}', + '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', + '{{y}}-{{MM}}-{{dd}}/{{filename}}', + '{{y}}-{{MMM}}-{{dd}}/{{filename}}', + '{{y}}-{{MMMM}}-{{dd}}/{{filename}}', + '{{y}}/{{y}}-{{MM}}/{{filename}}', + '{{y}}/{{y}}-{{WW}}/{{filename}}', + '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}', + '{{y}}/{{y}}-{{MM}}/{{assetId}}', + '{{y}}/{{y}}-{{WW}}/{{assetId}}', + '{{album}}/{{filename}}', +]; + +type ModelInfo = { dimSize: number }; +export const CLIP_MODEL_INFO: Record = { + RN50__openai: { dimSize: 1024 }, + RN50__yfcc15m: { dimSize: 1024 }, + RN50__cc12m: { dimSize: 1024 }, + RN101__openai: { dimSize: 512 }, + RN101__yfcc15m: { dimSize: 512 }, + RN50x4__openai: { dimSize: 640 }, + RN50x16__openai: { dimSize: 768 }, + RN50x64__openai: { dimSize: 1024 }, + 'ViT-B-32__openai': { dimSize: 512 }, + 'ViT-B-32__laion2b_e16': { dimSize: 512 }, + 'ViT-B-32__laion400m_e31': { dimSize: 512 }, + 'ViT-B-32__laion400m_e32': { dimSize: 512 }, + 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 }, + 'ViT-B-16__openai': { dimSize: 512 }, + 'ViT-B-16__laion400m_e31': { dimSize: 512 }, + 'ViT-B-16__laion400m_e32': { dimSize: 512 }, + 'ViT-B-16-plus-240__laion400m_e31': { dimSize: 640 }, + 'ViT-B-16-plus-240__laion400m_e32': { dimSize: 640 }, + 'ViT-L-14__openai': { dimSize: 768 }, + 'ViT-L-14__laion400m_e31': { dimSize: 768 }, + 'ViT-L-14__laion400m_e32': { dimSize: 768 }, + 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 }, + 'ViT-L-14-336__openai': { dimSize: 768 }, + 'ViT-L-14-quickgelu__dfn2b': { dimSize: 768 }, + 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 }, + 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 }, + 'LABSE-Vit-L-14': { dimSize: 768 }, + 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 }, + 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 }, + 'XLM-Roberta-Large-Vit-L-14': { dimSize: 768 }, + 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 }, + 'nllb-clip-base-siglip__v1': { dimSize: 768 }, + 'nllb-clip-large-siglip__v1': { dimSize: 1152 }, +}; diff --git a/server/src/immich/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts similarity index 68% rename from server/src/immich/controllers/activity.controller.ts rename to server/src/controllers/activity.controller.ts index 0808c7d4d..a65b284ca 100644 --- a/server/src/immich/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -1,17 +1,17 @@ -import { AuthDto } from '@app/domain'; -import { - ActivityDto, - ActivitySearchDto, - ActivityService, - ActivityCreateDto as CreateDto, - ActivityResponseDto as ResponseDto, - ActivityStatisticsResponseDto as StatsResponseDto, -} from '@app/domain/activity'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; -import { Auth, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { + ActivityCreateDto, + ActivityDto, + ActivityResponseDto, + ActivitySearchDto, + ActivityStatisticsResponseDto, +} from 'src/dtos/activity.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { ActivityService } from 'src/services/activity.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Activity') @Controller('activity') @@ -20,21 +20,21 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { + getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } @Get('statistics') - getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { + getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { return this.service.getStatistics(auth, dto); } @Post() async createActivity( @Auth() auth: AuthDto, - @Body() dto: CreateDto, + @Body() dto: ActivityCreateDto, @Res({ passthrough: true }) res: Response, - ): Promise { + ): Promise { const { duplicate, value } = await this.service.create(auth, dto); if (duplicate) { res.status(HttpStatus.OK); diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/controllers/album.controller.ts similarity index 80% rename from server/src/immich/controllers/album.controller.ts rename to server/src/controllers/album.controller.ts index ea1c5a428..c4b11fbb4 100644 --- a/server/src/immich/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -1,21 +1,19 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { AddUsersDto, AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, - AlbumService, - AuthDto, - BulkIdResponseDto, - BulkIdsDto, - CreateAlbumDto as CreateDto, + CreateAlbumDto, GetAlbumsDto, - UpdateAlbumDto as UpdateDto, -} from '@app/domain'; -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe'; -import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; + UpdateAlbumDto, +} from 'src/dtos/album.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { AlbumService } from 'src/services/album.service'; +import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; @ApiTags('Album') @Controller('album') @@ -34,7 +32,7 @@ export class AlbumController { } @Post() - createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise { + createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } @@ -52,7 +50,7 @@ export class AlbumController { updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: UpdateDto, + @Body() dto: UpdateAlbumDto, ): Promise { return this.service.update(auth, id, dto); } diff --git a/server/src/immich/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts similarity index 76% rename from server/src/immich/controllers/api-key.controller.ts rename to server/src/controllers/api-key.controller.ts index 5b5072533..564b90387 100644 --- a/server/src/immich/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,15 +1,10 @@ -import { - APIKeyCreateDto, - APIKeyCreateResponseDto, - APIKeyResponseDto, - APIKeyService, - APIKeyUpdateDto, - AuthDto, -} from '@app/domain'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { APIKeyService } from 'src/services/api-key.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('API Key') @Controller('api-key') diff --git a/server/src/immich/controllers/app.controller.ts b/server/src/controllers/app.controller.ts similarity index 80% rename from server/src/immich/controllers/app.controller.ts rename to server/src/controllers/app.controller.ts index 68a61c34c..472d0da3f 100644 --- a/server/src/immich/controllers/app.controller.ts +++ b/server/src/controllers/app.controller.ts @@ -1,7 +1,7 @@ -import { SystemConfigService } from '@app/domain'; import { Controller, Get, Header } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; -import { PublicRoute } from '../app.guard'; +import { PublicRoute } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; @Controller() export class AppController { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/controllers/asset-v1.controller.ts similarity index 63% rename from server/src/immich/api-v1/asset/asset.controller.ts rename to server/src/controllers/asset-v1.controller.ts index 37b561490..2ba9aa7a0 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/controllers/asset-v1.controller.ts @@ -1,4 +1,3 @@ -import { AssetResponseDto, AuthDto } from '@app/domain'; import { Body, Controller, @@ -16,23 +15,28 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../../app.guard'; -import { sendFile } from '../../app.utils'; -import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; -import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors'; -import FileNotEmptyValidator from '../validation/file-not-empty-validator'; -import { AssetService as AssetServiceV1 } from './asset.service'; -import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; -import { AssetSearchDto } from './dto/asset-search.dto'; -import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetDto } from './dto/create-asset.dto'; -import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto'; -import { ServeFileDto } from './dto/serve-file.dto'; -import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto'; -import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; -import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; -import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { + AssetBulkUploadCheckResponseDto, + AssetFileUploadResponseDto, + CheckExistingAssetsResponseDto, + CuratedLocationsResponseDto, + CuratedObjectsResponseDto, +} from 'src/dtos/asset-v1-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetSearchDto, + CheckExistingAssetsDto, + CreateAssetDto, + GetAssetThumbnailDto, + ServeFileDto, +} from 'src/dtos/asset-v1.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor'; +import { AssetServiceV1 } from 'src/services/asset-v1.service'; +import { sendFile } from 'src/utils/file'; +import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; interface UploadFiles { assetData: ImmichFile[]; @@ -43,8 +47,8 @@ interface UploadFiles { @ApiTags('Asset') @Controller(Route.ASSET) @Authenticated() -export class AssetController { - constructor(private serviceV1: AssetServiceV1) {} +export class AssetControllerV1 { + constructor(private service: AssetServiceV1) {} @SharedLinkRoute() @Post('upload') @@ -73,7 +77,7 @@ export class AssetController { sidecarFile = mapToUploadFile(_sidecarFile); } - const responseDto = await this.serviceV1.uploadFile(auth, dto, file, livePhotoFile, sidecarFile); + const responseDto = await this.service.uploadFile(auth, dto, file, livePhotoFile, sidecarFile); if (responseDto.duplicate) { res.status(HttpStatus.OK); } @@ -91,7 +95,7 @@ export class AssetController { @Param() { id }: UUIDParamDto, @Query() dto: ServeFileDto, ) { - await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto)); + await sendFile(res, next, () => this.service.serveFile(auth, id, dto)); } @SharedLinkRoute() @@ -104,22 +108,22 @@ export class AssetController { @Param() { id }: UUIDParamDto, @Query() dto: GetAssetThumbnailDto, ) { - await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto)); + await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto)); } @Get('/curated-objects') getCuratedObjects(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getCuratedObject(auth); + return this.service.getCuratedObject(auth); } @Get('/curated-locations') getCuratedLocations(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getCuratedLocation(auth); + return this.service.getCuratedLocation(auth); } @Get('/search-terms') getAssetSearchTerms(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getAssetSearchTerm(auth); + return this.service.getAssetSearchTerm(auth); } /** @@ -133,7 +137,7 @@ export class AssetController { schema: { type: 'string' }, }) getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { - return this.serviceV1.getAllAssets(auth, dto); + return this.service.getAllAssets(auth, dto); } /** @@ -145,7 +149,7 @@ export class AssetController { @Auth() auth: AuthDto, @Body() dto: CheckExistingAssetsDto, ): Promise { - return this.serviceV1.checkExistingAssets(auth, dto); + return this.service.checkExistingAssets(auth, dto); } /** @@ -157,6 +161,6 @@ export class AssetController { @Auth() auth: AuthDto, @Body() dto: AssetBulkUploadCheckDto, ): Promise { - return this.serviceV1.bulkUploadCheck(auth, dto); + return this.service.bulkUploadCheck(auth, dto); } } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts similarity index 73% rename from server/src/immich/controllers/asset.controller.ts rename to server/src/controllers/asset.controller.ts index 39a36b175..8e446d23f 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -1,31 +1,24 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, AssetBulkUpdateDto, AssetJobsDto, - AssetResponseDto, - AssetService, AssetStatsDto, AssetStatsResponseDto, - AuthDto, DeviceIdDto, - MapMarkerDto, - MapMarkerResponseDto, - MemoryLaneDto, - MemoryLaneResponseDto, - MetadataSearchDto, RandomAssetsDto, - SearchService, - TimeBucketAssetDto, - TimeBucketDto, - TimeBucketResponseDto, - UpdateAssetDto as UpdateDto, - UpdateStackParentDto, -} from '@app/domain'; -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { Route } from '../interceptors'; -import { UUIDParamDto } from './dto/uuid-param.dto'; + UpdateAssetDto, +} from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto'; +import { UpdateStackParentDto } from 'src/dtos/stack.dto'; +import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Route } from 'src/middleware/file-upload.interceptor'; +import { AssetService } from 'src/services/asset.service'; +import { SearchService } from 'src/services/search.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Asset') @Controller('assets') @@ -77,18 +70,6 @@ export class AssetController { return this.service.getStatistics(auth, dto); } - @Authenticated({ isShared: true }) - @Get('time-buckets') - getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { - return this.service.getTimeBuckets(auth, dto); - } - - @Authenticated({ isShared: true }) - @Get('time-bucket') - getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { - return this.service.getTimeBucket(auth, dto) as Promise; - } - @Post('jobs') @HttpCode(HttpStatus.NO_CONTENT) runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise { @@ -120,7 +101,11 @@ export class AssetController { } @Put(':id') - updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise { + updateAsset( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UpdateAssetDto, + ): Promise { return this.service.update(auth, id, dto); } } diff --git a/server/src/immich/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts similarity index 83% rename from server/src/immich/controllers/audit.controller.ts rename to server/src/controllers/audit.controller.ts index 09b707b8a..1487e78d4 100644 --- a/server/src/immich/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -1,16 +1,16 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { AuditDeletesDto, AuditDeletesResponseDto, - AuditService, - AuthDto, FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto, -} from '@app/domain'; -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { AdminRoute, Auth, Authenticated } from '../app.guard'; +} from 'src/dtos/audit.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard'; +import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @Controller('audit') diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts similarity index 87% rename from server/src/immich/controllers/auth.controller.ts rename to server/src/controllers/auth.controller.ts index ac1fea2bc..9b4e7a3bc 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,25 +1,21 @@ -import { - AuthDeviceResponseDto, - AuthDto, - AuthService, - ChangePasswordDto, - IMMICH_ACCESS_COOKIE, - IMMICH_AUTH_TYPE_COOKIE, - IMMICH_IS_AUTHENTICATED, - LoginCredentialDto, - LoginDetails, - LoginResponseDto, - LogoutResponseDto, - SignUpDto, - UserResponseDto, - ValidateAccessTokenResponseDto, - mapUser, -} from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants'; +import { + AuthDeviceResponseDto, + AuthDto, + ChangePasswordDto, + LoginCredentialDto, + LoginResponseDto, + LogoutResponseDto, + SignUpDto, + ValidateAccessTokenResponseDto, +} from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Authentication') @Controller('auth') diff --git a/server/src/immich/controllers/download.controller.ts b/server/src/controllers/download.controller.ts similarity index 75% rename from server/src/immich/controllers/download.controller.ts rename to server/src/controllers/download.controller.ts index 743797f74..4e4bf09d1 100644 --- a/server/src/immich/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -1,10 +1,13 @@ -import { AssetIdsDto, AuthDto, DownloadInfoDto, DownloadResponseDto, DownloadService } from '@app/domain'; import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../app.guard'; -import { asStreamableFile, sendFile } from '../app.utils'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { DownloadService } from 'src/services/download.service'; +import { asStreamableFile, sendFile } from 'src/utils/file'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Download') @Controller('download') diff --git a/server/src/immich/controllers/face.controller.ts b/server/src/controllers/face.controller.ts similarity index 67% rename from server/src/immich/controllers/face.controller.ts rename to server/src/controllers/face.controller.ts index f4014713b..a3f33fb86 100644 --- a/server/src/immich/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -1,8 +1,10 @@ -import { AssetFaceResponseDto, AuthDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain'; import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { PersonService } from 'src/services/person.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Face') @Controller('face') diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts new file mode 100644 index 000000000..ce51aa4c0 --- /dev/null +++ b/server/src/controllers/index.ts @@ -0,0 +1,52 @@ +import { ActivityController } from 'src/controllers/activity.controller'; +import { AlbumController } from 'src/controllers/album.controller'; +import { APIKeyController } from 'src/controllers/api-key.controller'; +import { AppController } from 'src/controllers/app.controller'; +import { AssetControllerV1 } from 'src/controllers/asset-v1.controller'; +import { AssetController, AssetsController } from 'src/controllers/asset.controller'; +import { AuditController } from 'src/controllers/audit.controller'; +import { AuthController } from 'src/controllers/auth.controller'; +import { DownloadController } from 'src/controllers/download.controller'; +import { FaceController } from 'src/controllers/face.controller'; +import { JobController } from 'src/controllers/job.controller'; +import { LibraryController } from 'src/controllers/library.controller'; +import { MemoryController } from 'src/controllers/memory.controller'; +import { OAuthController } from 'src/controllers/oauth.controller'; +import { PartnerController } from 'src/controllers/partner.controller'; +import { PersonController } from 'src/controllers/person.controller'; +import { SearchController } from 'src/controllers/search.controller'; +import { ServerInfoController } from 'src/controllers/server-info.controller'; +import { SharedLinkController } from 'src/controllers/shared-link.controller'; +import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { TagController } from 'src/controllers/tag.controller'; +import { TimelineController } from 'src/controllers/timeline.controller'; +import { TrashController } from 'src/controllers/trash.controller'; +import { UserController } from 'src/controllers/user.controller'; + +export const controllers = [ + ActivityController, + AssetsController, + AssetControllerV1, + AssetController, + AppController, + AlbumController, + APIKeyController, + AuditController, + AuthController, + DownloadController, + FaceController, + JobController, + LibraryController, + MemoryController, + OAuthController, + PartnerController, + SearchController, + ServerInfoController, + SharedLinkController, + SystemConfigController, + TagController, + TimelineController, + TrashController, + UserController, + PersonController, +]; diff --git a/server/src/immich/controllers/job.controller.ts b/server/src/controllers/job.controller.ts similarity index 79% rename from server/src/immich/controllers/job.controller.ts rename to server/src/controllers/job.controller.ts index 413af44de..d6bd45b1e 100644 --- a/server/src/immich/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,7 +1,8 @@ -import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobService, JobStatusDto } from '@app/domain'; import { Body, Controller, Get, Param, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticated } from '../app.guard'; +import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { JobService } from 'src/services/job.service'; @ApiTags('Job') @Controller('jobs') diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/controllers/library.controller.ts similarity index 74% rename from server/src/immich/controllers/library.controller.ts rename to server/src/controllers/library.controller.ts index 2b509645c..70d357187 100644 --- a/server/src/immich/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -1,18 +1,18 @@ -import { - CreateLibraryDto as CreateDto, - LibraryService, - LibraryStatsResponseDto, - LibraryResponseDto as ResponseDto, - ScanLibraryDto, - SearchLibraryDto, - UpdateLibraryDto as UpdateDto, - ValidateLibraryDto, - ValidateLibraryResponseDto, -} from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AdminRoute, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { + CreateLibraryDto, + LibraryResponseDto, + LibraryStatsResponseDto, + ScanLibraryDto, + SearchLibraryDto, + UpdateLibraryDto, + ValidateLibraryDto, + ValidateLibraryResponseDto, +} from 'src/dtos/library.dto'; +import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { LibraryService } from 'src/services/library.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Library') @Controller('library') @@ -22,22 +22,22 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - getAllLibraries(@Query() dto: SearchLibraryDto): Promise { + getAllLibraries(@Query() dto: SearchLibraryDto): Promise { return this.service.getAll(dto); } @Post() - createLibrary(@Body() dto: CreateDto): Promise { + createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } @Put(':id') - updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise { + updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { return this.service.update(id, dto); } @Get(':id') - getLibrary(@Param() { id }: UUIDParamDto): Promise { + getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts new file mode 100644 index 000000000..771d70594 --- /dev/null +++ b/server/src/controllers/memory.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { MemoryService } from 'src/services/memory.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Memory') +@Controller('memories') +@Authenticated() +export class MemoryController { + constructor(private service: MemoryService) {} + + @Get() + searchMemories(@Auth() auth: AuthDto): Promise { + return this.service.search(auth); + } + + @Post() + createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Get(':id') + getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + updateMemory( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: MemoryUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(auth, id); + } + + @Put(':id/assets') + addMemoryAssets( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: BulkIdsDto, + ): Promise { + return this.service.addAssets(auth, id, dto); + } + + @Delete(':id/assets') + @HttpCode(HttpStatus.OK) + removeMemoryAssets( + @Auth() auth: AuthDto, + @Body() dto: BulkIdsDto, + @Param() { id }: UUIDParamDto, + ): Promise { + return this.service.removeAssets(auth, id, dto); + } +} diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts similarity index 89% rename from server/src/immich/controllers/oauth.controller.ts rename to server/src/controllers/oauth.controller.ts index c7a5717af..debbd4e67 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,17 +1,16 @@ +import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Request, Response } from 'express'; import { AuthDto, - AuthService, - LoginDetails, LoginResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, - UserResponseDto, -} from '@app/domain'; -import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { Request, Response } from 'express'; -import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; +} from 'src/dtos/auth.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; +import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; @ApiTags('OAuth') @Controller('oauth') diff --git a/server/src/immich/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts similarity index 75% rename from server/src/immich/controllers/partner.controller.ts rename to server/src/controllers/partner.controller.ts index 65d95438d..f654a7263 100644 --- a/server/src/immich/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -1,9 +1,11 @@ -import { AuthDto, PartnerDirection, PartnerService } from '@app/domain'; -import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerDirection } from 'src/interfaces/partner.interface'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { PartnerService } from 'src/services/partner.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Partner') @Controller('partner') diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/controllers/person.controller.ts similarity index 85% rename from server/src/immich/controllers/person.controller.ts rename to server/src/controllers/person.controller.ts index 3408aa6ec..c9128a1f7 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,24 +1,24 @@ +import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { NextFunction, Response } from 'express'; +import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceUpdateDto, - AssetResponseDto, - AuthDto, - BulkIdResponseDto, MergePersonDto, PeopleResponseDto, PeopleUpdateDto, PersonCreateDto, PersonResponseDto, PersonSearchDto, - PersonService, PersonStatisticsResponseDto, PersonUpdateDto, -} from '@app/domain'; -import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { NextFunction, Response } from 'express'; -import { Auth, Authenticated, FileResponse } from '../app.guard'; -import { sendFile } from '../app.utils'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +} from 'src/dtos/person.dto'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; +import { PersonService } from 'src/services/person.service'; +import { sendFile } from 'src/utils/file'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Person') @Controller('person') diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/controllers/search.controller.ts similarity index 77% rename from server/src/immich/controllers/search.controller.ts rename to server/src/controllers/search.controller.ts index d508531dd..eaf45be29 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -1,20 +1,21 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PersonResponseDto } from 'src/dtos/person.dto'; import { - AuthDto, MetadataSearchDto, - PersonResponseDto, PlacesResponseDto, SearchDto, SearchExploreResponseDto, SearchPeopleDto, SearchPlacesDto, SearchResponseDto, - SearchService, + SearchSuggestionRequestDto, SmartSearchDto, -} from '@app/domain'; -import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto'; -import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; +} from 'src/dtos/search.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { SearchService } from 'src/services/search.service'; @ApiTags('Search') @Controller('search') @@ -55,6 +56,11 @@ export class SearchController { return this.service.searchPlaces(dto); } + @Get('cities') + getAssetsByCity(@Auth() auth: AuthDto): Promise { + return this.service.getAssetsByCity(auth); + } + @Get('suggestions') getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { return this.service.getSearchSuggestions(auth, dto); diff --git a/server/src/immich/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts similarity index 89% rename from server/src/immich/controllers/server-info.controller.ts rename to server/src/controllers/server-info.controller.ts index 4987a8984..e32b0d191 100644 --- a/server/src/immich/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,17 +1,17 @@ +import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { ServerConfigDto, ServerFeaturesDto, ServerInfoResponseDto, - ServerInfoService, ServerMediaTypesResponseDto, ServerPingResponse, ServerStatsResponseDto, ServerThemeDto, ServerVersionResponseDto, -} from '@app/domain'; -import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { AdminRoute, Authenticated, PublicRoute } from '../app.guard'; +} from 'src/dtos/server-info.dto'; +import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard'; +import { ServerInfoService } from 'src/services/server-info.service'; @ApiTags('Server Info') @Controller('server-info') diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts similarity index 84% rename from server/src/immich/controllers/shared-link.controller.ts rename to server/src/controllers/shared-link.controller.ts index d265d018d..990f4e322 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,19 +1,19 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Request, Response } from 'express'; +import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants'; +import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { - AssetIdsDto, - AssetIdsResponseDto, - AuthDto, - IMMICH_SHARED_LINK_ACCESS_COOKIE, SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, - SharedLinkService, -} from '@app/domain'; -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { Request, Response } from 'express'; -import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +} from 'src/dtos/shared-link.dto'; +import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') @Controller('shared-link') diff --git a/server/src/immich/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts similarity index 75% rename from server/src/immich/controllers/system-config.controller.ts rename to server/src/controllers/system-config.controller.ts index 73cf2c3c0..0b46b82a5 100644 --- a/server/src/immich/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,8 +1,8 @@ -import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain'; -import { MapThemeDto } from '@app/domain/system-config/system-config-map-theme.dto'; import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AdminRoute, Authenticated } from '../app.guard'; +import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { AdminRoute, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; @ApiTags('System Config') @Controller('system-config') @@ -31,6 +31,7 @@ export class SystemConfigController { } @AdminRoute(false) + @SharedLinkRoute() @Get('map/style.json') getMapStyle(@Query() dto: MapThemeDto) { return this.service.getMapStyle(dto.theme); diff --git a/server/src/immich/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts similarity index 77% rename from server/src/immich/controllers/tag.controller.ts rename to server/src/controllers/tag.controller.ts index 0d0c563d4..1caed8d52 100644 --- a/server/src/immich/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -1,17 +1,13 @@ -import { - AssetIdsDto, - AssetIdsResponseDto, - AssetResponseDto, - AuthDto, - CreateTagDto, - TagResponseDto, - TagService, - UpdateTagDto, -} from '@app/domain'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { TagService } from 'src/services/tag.service'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('Tag') @Controller('tag') diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts new file mode 100644 index 000000000..173c6738d --- /dev/null +++ b/server/src/controllers/timeline.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { TimelineService } from 'src/services/timeline.service'; + +@ApiTags('Timeline') +@Controller('timeline') +@Authenticated() +export class TimelineController { + constructor(private service: TimelineService) {} + + @Authenticated({ isShared: true }) + @Get('buckets') + getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { + return this.service.getTimeBuckets(auth, dto); + } + + @Authenticated({ isShared: true }) + @Get('bucket') + getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { + return this.service.getTimeBucket(auth, dto) as Promise; + } +} diff --git a/server/src/immich/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts similarity index 76% rename from server/src/immich/controllers/trash.controller.ts rename to server/src/controllers/trash.controller.ts index b61893817..25df3543c 100644 --- a/server/src/immich/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -1,7 +1,9 @@ -import { AuthDto, BulkIdsDto, TrashService } from '@app/domain'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Auth, Authenticated } from '../app.guard'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { TrashService } from 'src/services/trash.service'; @ApiTags('Trash') @Controller('trash') diff --git a/server/src/immich/controllers/user.controller.ts b/server/src/controllers/user.controller.ts similarity index 77% rename from server/src/immich/controllers/user.controller.ts rename to server/src/controllers/user.controller.ts index 0b3828f5c..c108e8852 100644 --- a/server/src/immich/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -1,13 +1,3 @@ -import { - AuthDto, - CreateUserDto as CreateDto, - CreateProfileImageDto, - CreateProfileImageResponseDto, - DeleteUserDto, - UpdateUserDto as UpdateDto, - UserResponseDto, - UserService, -} from '@app/domain'; import { Body, Controller, @@ -26,10 +16,14 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { AdminRoute, Auth, Authenticated, FileResponse } from '../app.guard'; -import { sendFile } from '../app.utils'; -import { FileUploadInterceptor, Route } from '../interceptors'; -import { UUIDParamDto } from './dto/uuid-param.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; +import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto'; +import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; +import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor'; +import { UserService } from 'src/services/user.service'; +import { sendFile } from 'src/utils/file'; +import { UUIDParamDto } from 'src/validation'; @ApiTags('User') @Controller(Route.USER) @@ -54,7 +48,7 @@ export class UserController { @AdminRoute() @Post() - createUser(@Body() createUserDto: CreateDto): Promise { + createUser(@Body() createUserDto: CreateUserDto): Promise { return this.service.create(createUserDto); } @@ -82,7 +76,7 @@ export class UserController { // TODO: replace with @Put(':id') @Put() - updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateDto): Promise { + updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise { return this.service.update(auth, updateUserDto); } diff --git a/server/src/domain/access/access.core.ts b/server/src/cores/access.core.ts similarity index 90% rename from server/src/domain/access/access.core.ts rename to server/src/cores/access.core.ts index 40b01de1d..72644870d 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/cores/access.core.ts @@ -1,8 +1,8 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { SharedLinkEntity } from '../../infra/entities'; -import { AuthDto } from '../auth'; -import { setDifference, setIsEqual, setUnion } from '../domain.util'; -import { IAccessRepository } from '../repositories'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; export enum Permission { ACTIVITY_CREATE = 'activity.create', @@ -33,6 +33,10 @@ export enum Permission { TIMELINE_READ = 'timeline.read', TIMELINE_DOWNLOAD = 'timeline.download', + MEMORY_READ = 'memory.read', + MEMORY_WRITE = 'memory.write', + MEMORY_DELETE = 'memory.delete', + PERSON_READ = 'person.read', PERSON_WRITE = 'person.write', PERSON_MERGE = 'person.merge', @@ -84,7 +88,7 @@ export class AccessCore { * * @returns Set */ - async checkAccess(auth: AuthDto, permission: Permission, ids: Set | string[]) { + async checkAccess(auth: AuthDto, permission: Permission, ids: Set | string[]): Promise> { const idSet = Array.isArray(ids) ? new Set(ids) : ids; if (idSet.size === 0) { return new Set(); @@ -97,7 +101,11 @@ export class AccessCore { return this.checkAccessOther(auth, permission, idSet); } - private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set) { + private async checkAccessSharedLink( + sharedLink: SharedLinkEntity, + permission: Permission, + ids: Set, + ): Promise> { const sharedLinkId = sharedLink.id; switch (permission) { @@ -140,7 +148,7 @@ export class AccessCore { } } - private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set) { + private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set): Promise> { switch (permission) { // uses album id case Permission.ACTIVITY_CREATE: { @@ -255,6 +263,18 @@ export class AccessCore { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); } + case Permission.MEMORY_READ: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_WRITE: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + case Permission.PERSON_READ: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/cores/storage.core.spec.ts b/server/src/cores/storage.core.spec.ts new file mode 100644 index 000000000..16258f095 --- /dev/null +++ b/server/src/cores/storage.core.spec.ts @@ -0,0 +1,29 @@ +import { StorageCore } from 'src/cores/storage.core'; + +jest.mock('src/constants', () => ({ + APP_MEDIA_LOCATION: '/photos', +})); + +describe('StorageCore', () => { + describe('isImmichPath', () => { + it('should return true for APP_MEDIA_LOCATION path', () => { + const immichPath = '/photos'; + expect(StorageCore.isImmichPath(immichPath)).toBe(true); + }); + + it('should return true for paths within the APP_MEDIA_LOCATION', () => { + const immichPath = '/photos/new/'; + expect(StorageCore.isImmichPath(immichPath)).toBe(true); + }); + + it('should return false for paths outside the APP_MEDIA_LOCATION and same starts', () => { + const nonImmichPath = '/photos_new'; + expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false); + }); + + it('should return false for paths outside the APP_MEDIA_LOCATION', () => { + const nonImmichPath = '/some/other/path'; + expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false); + }); + }); +}); diff --git a/server/src/domain/storage/storage.core.ts b/server/src/cores/storage.core.ts similarity index 76% rename from server/src/domain/storage/storage.core.ts rename to server/src/cores/storage.core.ts index 36e600b24..035f90c91 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,16 +1,17 @@ -import { SystemConfigCore } from '@app/domain/system-config'; -import { AssetEntity, AssetPathType, PathType, PersonEntity, PersonPathType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { dirname, join, resolve } from 'node:path'; -import { APP_MEDIA_LOCATION } from '../domain.constant'; -import { - IAssetRepository, - ICryptoRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, -} from '../repositories'; +import { APP_MEDIA_LOCATION } from 'src/constants'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; export enum StorageFolder { ENCODED_VIDEO = 'encoded-video', @@ -34,7 +35,8 @@ export interface MoveRequest { }; } -type GeneratedAssetPath = AssetPathType.JPEG_THUMBNAIL | AssetPathType.WEBP_THUMBNAIL | AssetPathType.ENCODED_VIDEO; +export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL; +export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; let instance: StorageCore | null; @@ -46,8 +48,8 @@ export class StorageCore { private moveRepository: IMoveRepository, private personRepository: IPersonRepository, private cryptoRepository: ICryptoRepository, - private systemConfigRepository: ISystemConfigRepository, private repository: IStorageRepository, + systemConfigRepository: ISystemConfigRepository, ) { this.configCore = SystemConfigCore.create(systemConfigRepository); } @@ -66,8 +68,8 @@ export class StorageCore { moveRepository, personRepository, cryptoRepository, - configRepository, repository, + configRepository, ); } @@ -94,12 +96,8 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); } - static getLargeThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`); - } - - static getSmallThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`); + static getImagePath(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); } static getEncodedVideoPath(asset: AssetEntity) { @@ -115,41 +113,36 @@ export class StorageCore { } static isImmichPath(path: string) { - return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION)); + const resolvedPath = resolve(path); + const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION); + const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/'; + const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/') + ? resolvedAppMediaLocation + : resolvedAppMediaLocation + '/'; + return normalizedPath.startsWith(normalizedAppMediaLocation); } static isGeneratedAsset(path: string) { return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); } - async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { - const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; - switch (pathType) { - case AssetPathType.JPEG_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: resizePath, - newPath: StorageCore.getLargeThumbnailPath(asset), - }); - } - case AssetPathType.WEBP_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: webpPath, - newPath: StorageCore.getSmallThumbnailPath(asset), - }); - } - case AssetPathType.ENCODED_VIDEO: { - return this.moveFile({ - entityId, - pathType, - oldPath: encodedVideoPath, - newPath: StorageCore.getEncodedVideoPath(asset), - }); - } - } + async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { + const { id: entityId, previewPath, thumbnailPath } = asset; + return this.moveFile({ + entityId, + pathType, + oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, + newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + }); + } + + async moveAssetVideo(asset: AssetEntity) { + return this.moveFile({ + entityId: asset.id, + pathType: AssetPathType.ENCODED_VIDEO, + oldPath: asset.encodedVideoPath, + newPath: StorageCore.getEncodedVideoPath(asset), + }); } async movePersonFile(person: PersonEntity, pathType: PersonPathType) { @@ -286,19 +279,19 @@ export class StorageCore { private savePath(pathType: PathType, id: string, newPath: string) { switch (pathType) { case AssetPathType.ORIGINAL: { - return this.assetRepository.save({ id, originalPath: newPath }); + return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.JPEG_THUMBNAIL: { - return this.assetRepository.save({ id, resizePath: newPath }); + case AssetPathType.PREVIEW: { + return this.assetRepository.update({ id, previewPath: newPath }); } - case AssetPathType.WEBP_THUMBNAIL: { - return this.assetRepository.save({ id, webpPath: newPath }); + case AssetPathType.THUMBNAIL: { + return this.assetRepository.update({ id, thumbnailPath: newPath }); } case AssetPathType.ENCODED_VIDEO: { - return this.assetRepository.save({ id, encodedVideoPath: newPath }); + return this.assetRepository.update({ id, encodedVideoPath: newPath }); } case AssetPathType.SIDECAR: { - return this.assetRepository.save({ id, sidecarPath: newPath }); + return this.assetRepository.update({ id, sidecarPath: newPath }); } case PersonPathType.FACE: { return this.personRepository.update({ id, thumbnailPath: newPath }); diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/cores/system-config.core.ts similarity index 95% rename from server/src/domain/system-config/system-config.core.ts rename to server/src/cores/system-config.core.ts index 93a4937cb..3a1ea47bb 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -1,7 +1,16 @@ +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { CronExpression } from '@nestjs/schedule'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { load as loadYaml } from 'js-yaml'; +import * as _ from 'lodash'; +import { Subject } from 'rxjs'; +import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AudioCodec, - Colorspace, CQMode, + Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -11,18 +20,10 @@ import { TranscodeHWAccel, TranscodePolicy, VideoCodec, -} from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; -import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; -import { CronExpression } from '@nestjs/schedule'; -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { load as loadYaml } from 'js-yaml'; -import * as _ from 'lodash'; -import { Subject } from 'rxjs'; -import { QueueName } from '../job/job.constants'; -import { ISystemConfigRepository } from '../repositories'; -import { SystemConfigDto } from './dto'; +} from 'src/entities/system-config.entity'; +import { QueueName } from 'src/interfaces/job.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise; @@ -112,9 +113,11 @@ export const defaults = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, diff --git a/server/src/domain/user/user.core.ts b/server/src/cores/user.core.ts similarity index 88% rename from server/src/domain/user/user.core.ts rename to server/src/cores/user.core.ts index 6134e97ce..e8596db3e 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/cores/user.core.ts @@ -1,10 +1,12 @@ -import { LibraryType, UserEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import sanitize from 'sanitize-filename'; -import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories'; -import { UserResponseDto } from './response-dto'; - -const SALT_ROUNDS = 10; +import { SALT_ROUNDS } from 'src/constants'; +import { UserResponseDto } from 'src/dtos/user.dto'; +import { LibraryType } from 'src/entities/library.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; let instance: UserCore | null; diff --git a/server/src/infra/database.config.ts b/server/src/database.config.ts similarity index 93% rename from server/src/infra/database.config.ts rename to server/src/database.config.ts index 773e79f8a..867b7f4cb 100644 --- a/server/src/infra/database.config.ts +++ b/server/src/database.config.ts @@ -1,4 +1,4 @@ -import { DatabaseExtension } from '@app/domain/repositories/database.repository'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { DataSource } from 'typeorm'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; @@ -17,10 +17,10 @@ const urlOrParts = url export const databaseConfig: PostgresConnectionOptions = { type: 'postgres', entities: [__dirname + '/entities/*.entity.{js,ts}'], - synchronize: false, migrations: [__dirname + '/migrations/*.{js,ts}'], subscribers: [__dirname + '/subscribers/*.{js,ts}'], migrationsRun: false, + synchronize: false, connectTimeoutMS: 10_000, // 10 seconds parseInt8: true, ...urlOrParts, diff --git a/server/src/decorators.ts b/server/src/decorators.ts new file mode 100644 index 000000000..39da2aa2a --- /dev/null +++ b/server/src/decorators.ts @@ -0,0 +1,130 @@ +import { SetMetadata } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; +import _ from 'lodash'; +import { ServerAsyncEvent, ServerEvent } from 'src/interfaces/event.interface'; +import { setUnion } from 'src/utils/set'; + +// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the +// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching +// by a list of IDs) requires splitting the query into multiple chunks. +// We are rounding down this limit, as queries commonly include other filters and parameters. +export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500; + +/** + * Chunks an array or set into smaller collections of the same type and specified size. + * + * @param collection The collection to chunk. + * @param size The size of each chunk. + */ +function chunks(collection: Array, size: number): Array>; +function chunks(collection: Set, size: number): Array>; +function chunks(collection: Array | Set, size: number): Array> | Array> { + if (collection instanceof Set) { + const result = []; + let chunk = new Set(); + for (const element of collection) { + chunk.add(element); + if (chunk.size === size) { + result.push(chunk); + chunk = new Set(); + } + } + if (chunk.size > 0) { + result.push(chunk); + } + return result; + } else { + return _.chunk(collection, size); + } +} + +/** + * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection, + * to overcome the maximum number of parameters allowed by the database driver. + * + * @param options.paramIndex The index of the function parameter to chunk. Defaults to 0. + * @param options.flatten Whether to flatten the results. Defaults to false. + */ +export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator { + return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor.value; + const parameterIndex = options.paramIndex ?? 0; + descriptor.value = async function (...arguments_: any[]) { + const argument = arguments_[parameterIndex]; + + // Early return if argument length is less than or equal to the chunk size. + if ( + (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) || + (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE) + ) { + return await originalMethod.apply(this, arguments_); + } + + return Promise.all( + chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => { + await Reflect.apply(originalMethod, this, [ + ...arguments_.slice(0, parameterIndex), + chunk, + ...arguments_.slice(parameterIndex + 1), + ]); + }), + ).then((results) => (options.mergeFn ? options.mergeFn(results) : results)); + }; + }; +} + +export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator { + return Chunked({ ...options, mergeFn: _.flatten }); +} + +export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator { + return Chunked({ ...options, mergeFn: setUnion }); +} + +// https://stackoverflow.com/a/74898678 +export function DecorateAll( + decorator: ( + target: any, + propertyKey: string, + descriptor: TypedPropertyDescriptor, + ) => TypedPropertyDescriptor | void, +) { + return (target: any) => { + const descriptors = Object.getOwnPropertyDescriptors(target.prototype); + for (const [propName, descriptor] of Object.entries(descriptors)) { + const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor'; + if (!isMethod) { + continue; + } + decorator({ ...target, constructor: { ...target.constructor, name: target.name } as any }, propName, descriptor); + Object.defineProperty(target.prototype, propName, descriptor); + } + }; +} + +const UUID = '00000000-0000-4000-a000-000000000000'; + +export const DummyValue = { + UUID, + UUID_SET: new Set([UUID]), + PAGINATION: { take: 10, skip: 0 }, + EMAIL: 'user@immich.app', + STRING: 'abcdefghi', + BUFFER: Buffer.from('abcdefghi'), + DATE: new Date(), + TIME_BUCKET: '2024-01-01T00:00:00.000Z', +}; + +export const GENERATE_SQL_KEY = 'generate-sql-key'; + +export interface GenerateSqlQueries { + name?: string; + params: unknown[]; +} + +/** Decorator to enable versioning/tracking of generated Sql */ +export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); + +export const OnServerEvent = (event: ServerEvent | ServerAsyncEvent, options?: OnEventOptions) => + OnEvent(event, { suppressErrors: false, ...options }); diff --git a/server/src/domain/access/index.ts b/server/src/domain/access/index.ts deleted file mode 100644 index 80ae0c534..000000000 --- a/server/src/domain/access/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './access.core'; diff --git a/server/src/domain/activity/index.ts b/server/src/domain/activity/index.ts deleted file mode 100644 index f0d954014..000000000 --- a/server/src/domain/activity/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './activity.dto'; -export * from './activity.service'; diff --git a/server/src/domain/album/dto/album-add-users.dto.ts b/server/src/domain/album/dto/album-add-users.dto.ts deleted file mode 100644 index f238b9a05..000000000 --- a/server/src/domain/album/dto/album-add-users.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ArrayNotEmpty } from 'class-validator'; -import { ValidateUUID } from '../../domain.util'; - -export class AddUsersDto { - @ValidateUUID({ each: true }) - @ArrayNotEmpty() - sharedUserIds!: string[]; -} diff --git a/server/src/domain/album/dto/album-create.dto.ts b/server/src/domain/album/dto/album-create.dto.ts deleted file mode 100644 index bebbed20b..000000000 --- a/server/src/domain/album/dto/album-create.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { Optional, ValidateUUID } from '../../domain.util'; - -export class CreateAlbumDto { - @IsString() - @ApiProperty() - albumName!: string; - - @IsString() - @Optional() - description?: string; - - @ValidateUUID({ optional: true, each: true }) - sharedWithUserIds?: string[]; - - @ValidateUUID({ optional: true, each: true }) - assetIds?: string[]; -} diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts deleted file mode 100644 index 4f88cefbb..000000000 --- a/server/src/domain/album/dto/album-update.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AssetOrder } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; - -export class UpdateAlbumDto { - @Optional() - @IsString() - albumName?: string; - - @Optional() - @IsString() - description?: string; - - @ValidateUUID({ optional: true }) - albumThumbnailAssetId?: string; - - @ValidateBoolean({ optional: true }) - isActivityEnabled?: boolean; - - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) - order?: AssetOrder; -} diff --git a/server/src/domain/album/dto/album.dto.ts b/server/src/domain/album/dto/album.dto.ts deleted file mode 100644 index b7aad98b5..000000000 --- a/server/src/domain/album/dto/album.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from '../../domain.util'; - -export class AlbumInfoDto { - @ValidateBoolean({ optional: true }) - withoutAssets?: boolean; -} diff --git a/server/src/domain/album/dto/get-albums.dto.ts b/server/src/domain/album/dto/get-albums.dto.ts deleted file mode 100644 index 2628a3fc7..000000000 --- a/server/src/domain/album/dto/get-albums.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ValidateBoolean, ValidateUUID } from '../../domain.util'; - -export class GetAlbumsDto { - @ValidateBoolean({ optional: true }) - /** - * true: only shared albums - * false: only non-shared own albums - * undefined: shared and owned albums - */ - shared?: boolean; - - /** - * Only returns albums that contain the asset - * Ignores the shared parameter - * undefined: get all albums - */ - @ValidateUUID({ optional: true }) - assetId?: string; -} diff --git a/server/src/domain/album/dto/index.ts b/server/src/domain/album/dto/index.ts deleted file mode 100644 index b1a4c2141..000000000 --- a/server/src/domain/album/dto/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './album-add-users.dto'; -export * from './album-create.dto'; -export * from './album-update.dto'; -export * from './album.dto'; -export * from './get-albums.dto'; diff --git a/server/src/domain/album/index.ts b/server/src/domain/album/index.ts deleted file mode 100644 index f06f6d33c..000000000 --- a/server/src/domain/album/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './album-response.dto'; -export * from './album.service'; -export * from './dto'; diff --git a/server/src/domain/api-key/index.ts b/server/src/domain/api-key/index.ts deleted file mode 100644 index 94076f2a3..000000000 --- a/server/src/domain/api-key/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './api-key.dto'; -export * from './api-key.service'; diff --git a/server/src/domain/asset/dto/asset-ids.dto.ts b/server/src/domain/asset/dto/asset-ids.dto.ts deleted file mode 100644 index 5ee988bb4..000000000 --- a/server/src/domain/asset/dto/asset-ids.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { ValidateUUID } from '../../domain.util'; - -export class AssetIdsDto { - @ValidateUUID({ each: true }) - assetIds!: string[]; -} - -export enum AssetJobName { - REGENERATE_THUMBNAIL = 'regenerate-thumbnail', - REFRESH_METADATA = 'refresh-metadata', - TRANSCODE_VIDEO = 'transcode-video', -} - -export class AssetJobsDto extends AssetIdsDto { - @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) - @IsEnum(AssetJobName) - name!: AssetJobName; -} diff --git a/server/src/domain/asset/dto/asset-statistics.dto.ts b/server/src/domain/asset/dto/asset-statistics.dto.ts deleted file mode 100644 index c313ccdf4..000000000 --- a/server/src/domain/asset/dto/asset-statistics.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AssetType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateBoolean } from '../../domain.util'; -import { AssetStats } from '../../repositories'; - -export class AssetStatsDto { - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isTrashed?: boolean; -} - -export class AssetStatsResponseDto { - @ApiProperty({ type: 'integer' }) - images!: number; - - @ApiProperty({ type: 'integer' }) - videos!: number; - - @ApiProperty({ type: 'integer' }) - total!: number; -} - -export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { - return { - images: stats[AssetType.IMAGE], - videos: stats[AssetType.VIDEO], - total: Object.values(stats).reduce((total, value) => total + value, 0), - }; -}; diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts deleted file mode 100644 index 2abe31d0a..000000000 --- a/server/src/domain/asset/dto/asset.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Type } from 'class-transformer'; -import { - IsDateString, - IsInt, - IsLatitude, - IsLongitude, - IsNotEmpty, - IsPositive, - IsString, - ValidateIf, -} from 'class-validator'; -import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; -import { BulkIdsDto } from '../response-dto'; - -export class DeviceIdDto { - @IsNotEmpty() - @IsString() - deviceId!: string; -} - -const hasGPS = (o: { latitude: undefined; longitude: undefined }) => - o.latitude !== undefined || o.longitude !== undefined; -const ValidateGPS = () => ValidateIf(hasGPS); - -export class UpdateAssetBase { - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @Optional() - @IsDateString() - dateTimeOriginal?: string; - - @ValidateGPS() - @IsLatitude() - @IsNotEmpty() - latitude?: number; - - @ValidateGPS() - @IsLongitude() - @IsNotEmpty() - longitude?: number; -} - -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true }) - ids!: string[]; - - @ValidateUUID({ optional: true }) - stackParentId?: string; - - @ValidateBoolean({ optional: true }) - removeParent?: boolean; -} - -export class UpdateAssetDto extends UpdateAssetBase { - @Optional() - @IsString() - description?: string; -} - -export class RandomAssetsDto { - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} - -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true }) - force?: boolean; -} diff --git a/server/src/domain/asset/dto/index.ts b/server/src/domain/asset/dto/index.ts deleted file mode 100644 index bc7a100b9..000000000 --- a/server/src/domain/asset/dto/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './asset-ids.dto'; -export * from './asset-stack.dto'; -export * from './asset-statistics.dto'; -export * from './asset.dto'; -export * from './map-marker.dto'; -export * from './memory-lane.dto'; -export * from './time-bucket.dto'; diff --git a/server/src/domain/asset/dto/map-marker.dto.ts b/server/src/domain/asset/dto/map-marker.dto.ts deleted file mode 100644 index 4fe6c16b8..000000000 --- a/server/src/domain/asset/dto/map-marker.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ValidateBoolean, ValidateDate } from '../../domain.util'; - -export class MapMarkerDto { - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateDate({ optional: true }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true }) - withPartners?: boolean; -} diff --git a/server/src/domain/asset/dto/memory-lane.dto.ts b/server/src/domain/asset/dto/memory-lane.dto.ts deleted file mode 100644 index 43f74aff1..000000000 --- a/server/src/domain/asset/dto/memory-lane.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Max, Min } from 'class-validator'; - -export class MemoryLaneDto { - @IsInt() - @Type(() => Number) - @Max(31) - @Min(1) - @ApiProperty({ type: 'integer' }) - day!: number; - - @IsInt() - @Type(() => Number) - @Max(12) - @Min(1) - @ApiProperty({ type: 'integer' }) - month!: number; -} diff --git a/server/src/domain/asset/index.ts b/server/src/domain/asset/index.ts deleted file mode 100644 index 71ad3c8c4..000000000 --- a/server/src/domain/asset/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './asset.service'; -export * from './dto'; -export * from './response-dto'; diff --git a/server/src/domain/asset/response-dto/index.ts b/server/src/domain/asset/response-dto/index.ts deleted file mode 100644 index 7ed99db13..000000000 --- a/server/src/domain/asset/response-dto/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './asset-ids-response.dto'; -export * from './asset-response.dto'; -export * from './exif-response.dto'; -export * from './map-marker-response.dto'; -export * from './smart-info-response.dto'; -export * from './time-bucket-response.dto'; diff --git a/server/src/domain/asset/response-dto/map-marker-response.dto.ts b/server/src/domain/asset/response-dto/map-marker-response.dto.ts deleted file mode 100644 index f5148883f..000000000 --- a/server/src/domain/asset/response-dto/map-marker-response.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class MapMarkerResponseDto { - @ApiProperty() - id!: string; - - @ApiProperty({ format: 'double' }) - lat!: number; - - @ApiProperty({ format: 'double' }) - lon!: number; - - @ApiProperty() - city!: string | null; - - @ApiProperty() - state!: string | null; - - @ApiProperty() - country!: string | null; -} diff --git a/server/src/domain/asset/response-dto/smart-info-response.dto.ts b/server/src/domain/asset/response-dto/smart-info-response.dto.ts deleted file mode 100644 index 72c336205..000000000 --- a/server/src/domain/asset/response-dto/smart-info-response.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SmartInfoEntity } from '@app/infra/entities'; - -export class SmartInfoResponseDto { - tags?: string[] | null; - objects?: string[] | null; -} - -export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto { - return { - tags: entity.tags, - objects: entity.objects, - }; -} diff --git a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts b/server/src/domain/asset/response-dto/time-bucket-response.dto.ts deleted file mode 100644 index e143dde46..000000000 --- a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class TimeBucketResponseDto { - @ApiProperty({ type: 'string' }) - timeBucket!: string; - - @ApiProperty({ type: 'integer' }) - count!: number; -} diff --git a/server/src/domain/audit/index.ts b/server/src/domain/audit/index.ts deleted file mode 100644 index febebf0f6..000000000 --- a/server/src/domain/audit/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './audit.dto'; -export * from './audit.service'; diff --git a/server/src/domain/auth/auth.constant.ts b/server/src/domain/auth/auth.constant.ts deleted file mode 100644 index f29fc9274..000000000 --- a/server/src/domain/auth/auth.constant.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const MOBILE_REDIRECT = 'app.immich:/'; -export const LOGIN_URL = '/auth/login?autoLaunch=0'; -export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; -export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; -export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; -export const IMMICH_API_KEY_NAME = 'api_key'; -export const IMMICH_API_KEY_HEADER = 'x-api-key'; -export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; -export enum AuthType { - PASSWORD = 'password', - OAUTH = 'oauth', -} diff --git a/server/src/domain/auth/index.ts b/server/src/domain/auth/index.ts deleted file mode 100644 index 52e0463bc..000000000 --- a/server/src/domain/auth/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './auth.constant'; -export * from './auth.dto'; -export * from './auth.service'; diff --git a/server/src/domain/database/index.ts b/server/src/domain/database/index.ts deleted file mode 100644 index cd4e1d217..000000000 --- a/server/src/domain/database/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './database.service'; diff --git a/server/src/domain/domain.config.ts b/server/src/domain/domain.config.ts deleted file mode 100644 index b0471080d..000000000 --- a/server/src/domain/domain.config.ts +++ /dev/null @@ -1,36 +0,0 @@ -// TODO: remove nestjs references from domain -import { LogLevel } from '@app/infra/entities'; -import { ConfigModuleOptions } from '@nestjs/config'; -import Joi from 'joi'; - -const WHEN_DB_URL_SET = Joi.when('DB_URL', { - is: Joi.exist(), - then: Joi.string().optional(), - otherwise: Joi.string().required(), -}); - -export const immichAppConfig: ConfigModuleOptions = { - envFilePath: '.env', - isGlobal: true, - validationSchema: Joi.object({ - NODE_ENV: Joi.string().optional().valid('development', 'production', 'staging').default('development'), - LOG_LEVEL: Joi.string() - .optional() - .valid(...Object.values(LogLevel)), - - DB_USERNAME: WHEN_DB_URL_SET, - DB_PASSWORD: WHEN_DB_URL_SET, - DB_DATABASE_NAME: WHEN_DB_URL_SET, - DB_URL: Joi.string().optional(), - DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'), - - MACHINE_LEARNING_PORT: Joi.number().optional(), - MICROSERVICES_PORT: Joi.number().optional(), - IMMICH_METRICS_PORT: Joi.number().optional(), - - IMMICH_METRICS: Joi.boolean().optional().default(false), - IMMICH_HOST_METRICS: Joi.boolean().optional().default(false), - IMMICH_API_METRICS: Joi.boolean().optional().default(false), - IMMICH_IO_METRICS: Joi.boolean().optional().default(false), - }), -}; diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts deleted file mode 100644 index c3e62edb5..000000000 --- a/server/src/domain/domain.module.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ImmichLogger } from '@app/infra/logger'; -import { Global, Module, Provider } from '@nestjs/common'; -import { ActivityService } from './activity'; -import { AlbumService } from './album'; -import { APIKeyService } from './api-key'; -import { AssetService } from './asset'; -import { AuditService } from './audit'; -import { AuthService } from './auth'; -import { DatabaseService } from './database'; -import { DownloadService } from './download'; -import { JobService } from './job'; -import { LibraryService } from './library'; -import { MediaService } from './media'; -import { MetadataService } from './metadata'; -import { PartnerService } from './partner'; -import { PersonService } from './person'; -import { SearchService } from './search'; -import { ServerInfoService } from './server-info'; -import { SharedLinkService } from './shared-link'; -import { SmartInfoService } from './smart-info'; -import { StorageService } from './storage'; -import { StorageTemplateService } from './storage-template'; -import { SystemConfigService } from './system-config'; -import { TagService } from './tag'; -import { TrashService } from './trash'; -import { UserService } from './user'; - -const providers: Provider[] = [ - APIKeyService, - ActivityService, - AlbumService, - AssetService, - AuditService, - AuthService, - DatabaseService, - DownloadService, - ImmichLogger, - JobService, - LibraryService, - MediaService, - MetadataService, - PartnerService, - PersonService, - SearchService, - ServerInfoService, - SharedLinkService, - SmartInfoService, - StorageService, - StorageTemplateService, - SystemConfigService, - TagService, - TrashService, - UserService, -]; - -@Global() -@Module({ - imports: [], - providers: [...providers], - exports: [...providers], -}) -export class DomainModule {} diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts deleted file mode 100644 index a079ff6bf..000000000 --- a/server/src/domain/domain.util.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { ImmichLogger } from '@app/infra/logger'; -import { BadRequestException, applyDecorators } from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsDate, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, - ValidateIf, - ValidationOptions, - isDateString, -} from 'class-validator'; -import { CronJob } from 'cron'; -import _ from 'lodash'; -import { basename, extname } from 'node:path'; -import sanitize from 'sanitize-filename'; - -export enum CacheControl { - PRIVATE_WITH_CACHE = 'private_with_cache', - PRIVATE_WITHOUT_CACHE = 'private_without_cache', - NONE = 'none', -} - -export class ImmichFileResponse { - public readonly path!: string; - public readonly contentType!: string; - public readonly cacheControl!: CacheControl; - - constructor(response: ImmichFileResponse) { - Object.assign(this, response); - } -} - -export interface OpenGraphTags { - title: string; - description: string; - imageUrl?: string; -} - -export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; - -type UUIDOptions = { optional?: boolean; each?: boolean }; -export const ValidateUUID = (options?: UUIDOptions) => { - const { optional, each } = { optional: false, each: false, ...options }; - return applyDecorators( - IsUUID('4', { each }), - ApiProperty({ format: 'uuid' }), - optional ? Optional() : IsNotEmpty(), - each ? IsArray() : IsString(), - ); -}; - -type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions) => { - const { optional, nullable, format } = { optional: false, nullable: false, format: 'date-time', ...options }; - - const decorators = [ - ApiProperty({ format }), - IsDate(), - optional ? Optional({ nullable: true }) : IsNotEmpty(), - Transform(({ key, value }) => { - if (value === null || value === undefined) { - return value; - } - - if (!isDateString(value)) { - throw new BadRequestException(`${key} must be a date string`); - } - - return new Date(value as string); - }), - ]; - - if (optional) { - decorators.push(Optional({ nullable })); - } - - return applyDecorators(...decorators); -}; - -type BooleanOptions = { optional?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions) => { - const { optional } = { optional: false, ...options }; - const decorators = [ - // ApiProperty(), - IsBoolean(), - Transform(({ value }) => { - if (value == 'true') { - return true; - } else if (value == 'false') { - return false; - } - return value; - }), - ]; - - if (optional) { - decorators.push(Optional()); - } - - return applyDecorators(...decorators); -}; - -export function validateCronExpression(expression: string) { - try { - new CronJob(expression, () => {}); - } catch { - return false; - } - - return true; -} - -type IValue = { value: string }; - -export const toEmail = ({ value }: IValue) => value?.toLowerCase(); - -export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', '')); - -export function getFileNameWithoutExtension(path: string): string { - return basename(path, extname(path)); -} - -export function getLivePhotoMotionFilename(stillName: string, motionName: string) { - return getFileNameWithoutExtension(stillName) + extname(motionName); -} - -const KiB = Math.pow(1024, 1); -const MiB = Math.pow(1024, 2); -const GiB = Math.pow(1024, 3); -const TiB = Math.pow(1024, 4); -const PiB = Math.pow(1024, 5); - -export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; - -export function asHumanReadable(bytes: number, precision = 1): string { - const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; - - let magnitude = 0; - let remainder = bytes; - while (remainder >= 1024) { - if (magnitude + 1 < units.length) { - magnitude++; - remainder /= 1024; - } else { - break; - } - } - - return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; -} - -export interface PaginationOptions { - take: number; - skip?: number; -} - -export enum PaginationMode { - LIMIT_OFFSET = 'limit-offset', - SKIP_TAKE = 'skip-take', -} - -export interface PaginatedBuilderOptions { - take: number; - skip?: number; - mode?: PaginationMode; -} - -export interface PaginationResult { - items: T[]; - hasNextPage: boolean; -} - -export type Paginated = Promise>; - -export async function* usePagination( - pageSize: number, - getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, -) { - let hasNextPage = true; - - for (let skip = 0; hasNextPage; skip += pageSize) { - const result = await getNextPage({ take: pageSize, skip }); - hasNextPage = result.hasNextPage; - yield result.items; - } -} - -export interface OptionalOptions extends ValidationOptions { - nullable?: boolean; -} - -/** - * Checks if value is missing and if so, ignores all validators. - * - * @param validationOptions {@link OptionalOptions} - * - * @see IsOptional exported from `class-validator. - */ -// https://stackoverflow.com/a/71353929 -export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) { - if (nullable === true) { - return IsOptional(validationOptions); - } - - return ValidateIf((object: any, v: any) => v !== undefined, validationOptions); -} - -/** - * Chunks an array or set into smaller collections of the same type and specified size. - * - * @param collection The collection to chunk. - * @param size The size of each chunk. - */ -export function chunks(collection: Array, size: number): Array>; -export function chunks(collection: Set, size: number): Array>; -export function chunks(collection: Array | Set, size: number): Array> | Array> { - if (collection instanceof Set) { - const result = []; - let chunk = new Set(); - for (const element of collection) { - chunk.add(element); - if (chunk.size === size) { - result.push(chunk); - chunk = new Set(); - } - } - if (chunk.size > 0) { - result.push(chunk); - } - return result; - } else { - return _.chunk(collection, size); - } -} - -// NOTE: The following Set utils have been added here, to easily determine where they are used. -// They should be replaced with native Set operations, when they are added to the language. -// Proposal reference: https://github.com/tc39/proposal-set-methods - -export const setUnion = (...sets: Set[]): Set => { - const union = new Set(sets[0]); - for (const set of sets.slice(1)) { - for (const element of set) { - union.add(element); - } - } - return union; -}; - -export const setDifference = (setA: Set, ...sets: Set[]): Set => { - const difference = new Set(setA); - for (const set of sets) { - for (const element of set) { - difference.delete(element); - } - } - return difference; -}; - -export const setIsSuperset = (set: Set, subset: Set): boolean => { - for (const element of subset) { - if (!set.has(element)) { - return false; - } - } - return true; -}; - -export const setIsEqual = (setA: Set, setB: Set): boolean => { - return setA.size === setB.size && setIsSuperset(setA, setB); -}; - -export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => { - promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); -}; diff --git a/server/src/domain/download/index.ts b/server/src/domain/download/index.ts deleted file mode 100644 index ab5c91ec9..000000000 --- a/server/src/domain/download/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './download.dto'; -export * from './download.service'; diff --git a/server/src/domain/index.ts b/server/src/domain/index.ts deleted file mode 100644 index dce2fa696..000000000 --- a/server/src/domain/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -export * from './access'; -export * from './activity'; -export * from './album'; -export * from './api-key'; -export * from './asset'; -export * from './audit'; -export * from './auth'; -export * from './database'; -export * from './domain.config'; -export * from './domain.constant'; -export * from './domain.module'; -export * from './domain.util'; -export * from './download'; -export * from './job'; -export * from './library'; -export * from './media'; -export * from './metadata'; -export * from './partner'; -export * from './person'; -export * from './repositories'; -export * from './search'; -export * from './server-info'; -export * from './shared-link'; -export * from './smart-info'; -export * from './storage'; -export * from './storage-template'; -export * from './system-config'; -export * from './tag'; -export * from './trash'; -export * from './user'; diff --git a/server/src/domain/job/index.ts b/server/src/domain/job/index.ts deleted file mode 100644 index 44f617f0c..000000000 --- a/server/src/domain/job/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './job.constants'; -export * from './job.dto'; -export * from './job.interface'; -export * from './job.service'; diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts deleted file mode 100644 index fe3c81727..000000000 --- a/server/src/domain/job/job.constants.ts +++ /dev/null @@ -1,155 +0,0 @@ -export enum QueueName { - THUMBNAIL_GENERATION = 'thumbnailGeneration', - METADATA_EXTRACTION = 'metadataExtraction', - VIDEO_CONVERSION = 'videoConversion', - FACE_DETECTION = 'faceDetection', - FACIAL_RECOGNITION = 'facialRecognition', - SMART_SEARCH = 'smartSearch', - BACKGROUND_TASK = 'backgroundTask', - STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', - MIGRATION = 'migration', - SEARCH = 'search', - SIDECAR = 'sidecar', - LIBRARY = 'library', -} - -export type ConcurrentQueueName = Exclude< - QueueName, - QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION ->; - -export enum JobCommand { - START = 'start', - PAUSE = 'pause', - RESUME = 'resume', - EMPTY = 'empty', - CLEAR_FAILED = 'clear-failed', -} - -export enum JobName { - // conversion - QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', - VIDEO_CONVERSION = 'video-conversion', - - // thumbnails - QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', - GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', - GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', - GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', - - // metadata - QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', - METADATA_EXTRACTION = 'metadata-extraction', - LINK_LIVE_PHOTOS = 'link-live-photos', - - // user - USER_DELETION = 'user-deletion', - USER_DELETE_CHECK = 'user-delete-check', - USER_SYNC_USAGE = 'user-sync-usage', - - // asset - ASSET_DELETION = 'asset-deletion', - ASSET_DELETION_CHECK = 'asset-deletion-check', - - // storage template - STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', - STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', - - // migration - QUEUE_MIGRATION = 'queue-migration', - MIGRATE_ASSET = 'migrate-asset', - MIGRATE_PERSON = 'migrate-person', - - // facial recognition - PERSON_CLEANUP = 'person-cleanup', - QUEUE_FACE_DETECTION = 'queue-face-detection', - FACE_DETECTION = 'face-detection', - QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', - FACIAL_RECOGNITION = 'facial-recognition', - - // library managment - LIBRARY_SCAN = 'library-refresh', - LIBRARY_SCAN_ASSET = 'library-refresh-asset', - LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', - LIBRARY_DELETE = 'library-delete', - LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', - LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', - - // cleanup - DELETE_FILES = 'delete-files', - CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', - - // smart search - QUEUE_SMART_SEARCH = 'queue-smart-search', - SMART_SEARCH = 'smart-search', - - // XMP sidecars - QUEUE_SIDECAR = 'queue-sidecar', - SIDECAR_DISCOVERY = 'sidecar-discovery', - SIDECAR_SYNC = 'sidecar-sync', - SIDECAR_WRITE = 'sidecar-write', -} - -export const JOBS_ASSET_PAGINATION_SIZE = 1000; - -export const JOBS_TO_QUEUE: Record = { - // misc - [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK, - [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK, - [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK, - [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK, - [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK, - [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, - [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, - [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK, - - // conversion - [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, - [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, - - // thumbnails - [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - - // metadata - [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, - [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, - [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION, - - // storage template - [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION, - [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION, - - // migration - [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION, - [JobName.MIGRATE_ASSET]: QueueName.MIGRATION, - [JobName.MIGRATE_PERSON]: QueueName.MIGRATION, - - // facial recognition - [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION, - [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION, - [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, - [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, - - // smart search - [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH, - [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH, - - // XMP sidecars - [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, - [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, - [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, - [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, - - // Library management - [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, - [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, - [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, - [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, - [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, - [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, -}; diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts deleted file mode 100644 index 8e7420bf9..000000000 --- a/server/src/domain/job/job.interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface IBaseJob { - force?: boolean; -} - -export interface IEntityJob extends IBaseJob { - id: string; - source?: 'upload' | 'sidecar-write'; -} - -export interface IAssetDeletionJob extends IEntityJob { - fromExternal?: boolean; -} - -export interface ILibraryFileJob extends IEntityJob { - ownerId: string; - assetPath: string; -} - -export interface ILibraryRefreshJob extends IEntityJob { - refreshModifiedFiles: boolean; - refreshAllFiles: boolean; -} - -export interface IBulkEntityJob extends IBaseJob { - ids: string[]; -} - -export interface IDeleteFilesJob extends IBaseJob { - files: Array; -} - -export interface ISidecarWriteJob extends IEntityJob { - description?: string; - dateTimeOriginal?: string; - latitude?: number; - longitude?: number; -} - -export interface IDeferrableJob extends IEntityJob { - deferred?: boolean; -} diff --git a/server/src/domain/library/index.ts b/server/src/domain/library/index.ts deleted file mode 100644 index da0d981f2..000000000 --- a/server/src/domain/library/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './library.dto'; -export * from './library.service'; diff --git a/server/src/domain/media/index.ts b/server/src/domain/media/index.ts deleted file mode 100644 index 83a31567b..000000000 --- a/server/src/domain/media/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './media.constant'; -export * from './media.service'; diff --git a/server/src/domain/media/media.constant.ts b/server/src/domain/media/media.constant.ts deleted file mode 100644 index 3a8ee414b..000000000 --- a/server/src/domain/media/media.constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const FACE_THUMBNAIL_SIZE = 250; diff --git a/server/src/domain/metadata/index.ts b/server/src/domain/metadata/index.ts deleted file mode 100644 index 92c69e450..000000000 --- a/server/src/domain/metadata/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './metadata.service'; diff --git a/server/src/domain/partner/index.ts b/server/src/domain/partner/index.ts deleted file mode 100644 index b25925e89..000000000 --- a/server/src/domain/partner/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './partner.dto'; -export * from './partner.service'; diff --git a/server/src/domain/person/index.ts b/server/src/domain/person/index.ts deleted file mode 100644 index 14a960467..000000000 --- a/server/src/domain/person/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './person.dto'; -export * from './person.service'; diff --git a/server/src/domain/repositories/communication.repository.ts b/server/src/domain/repositories/communication.repository.ts deleted file mode 100644 index 3efbbcb5e..000000000 --- a/server/src/domain/repositories/communication.repository.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AssetResponseDto, ReleaseNotification, ServerVersionResponseDto } from '@app/domain'; -import { SystemConfig } from '@app/infra/entities'; - -export const ICommunicationRepository = 'ICommunicationRepository'; - -export enum ClientEvent { - UPLOAD_SUCCESS = 'on_upload_success', - USER_DELETE = 'on_user_delete', - ASSET_DELETE = 'on_asset_delete', - ASSET_TRASH = 'on_asset_trash', - ASSET_UPDATE = 'on_asset_update', - ASSET_HIDDEN = 'on_asset_hidden', - ASSET_RESTORE = 'on_asset_restore', - ASSET_STACK_UPDATE = 'on_asset_stack_update', - PERSON_THUMBNAIL = 'on_person_thumbnail', - SERVER_VERSION = 'on_server_version', - CONFIG_UPDATE = 'on_config_update', - NEW_RELEASE = 'on_new_release', -} - -export enum ServerEvent { - CONFIG_UPDATE = 'config:update', -} - -export enum InternalEvent { - VALIDATE_CONFIG = 'validate_config', -} - -export interface InternalEventMap { - [InternalEvent.VALIDATE_CONFIG]: { newConfig: SystemConfig; oldConfig: SystemConfig }; -} - -export interface ClientEventMap { - [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; - [ClientEvent.USER_DELETE]: string; - [ClientEvent.ASSET_DELETE]: string; - [ClientEvent.ASSET_TRASH]: string[]; - [ClientEvent.ASSET_UPDATE]: AssetResponseDto; - [ClientEvent.ASSET_HIDDEN]: string; - [ClientEvent.ASSET_RESTORE]: string[]; - [ClientEvent.ASSET_STACK_UPDATE]: string[]; - [ClientEvent.PERSON_THUMBNAIL]: string; - [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; - [ClientEvent.CONFIG_UPDATE]: Record; - [ClientEvent.NEW_RELEASE]: ReleaseNotification; -} - -export type OnConnectCallback = (userId: string) => void | Promise; -export type OnServerEventCallback = () => Promise; - -export interface ICommunicationRepository { - send(event: E, userId: string, data: ClientEventMap[E]): void; - broadcast(event: E, data: ClientEventMap[E]): void; - on(event: 'connect', callback: OnConnectCallback): void; - on(event: ServerEvent, callback: OnServerEventCallback): void; - sendServerEvent(event: ServerEvent): void; - emit(event: E, data: InternalEventMap[E]): boolean; - emitAsync(event: E, data: InternalEventMap[E]): Promise; -} diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts deleted file mode 100644 index 636abd2be..000000000 --- a/server/src/domain/repositories/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export * from './access.repository'; -export * from './activity.repository'; -export * from './album.repository'; -export * from './api-key.repository'; -export * from './asset-stack.repository'; -export * from './asset.repository'; -export * from './audit.repository'; -export * from './communication.repository'; -export * from './crypto.repository'; -export * from './database.repository'; -export * from './job.repository'; -export * from './library.repository'; -export * from './machine-learning.repository'; -export * from './media.repository'; -export * from './metadata.repository'; -export * from './move.repository'; -export * from './partner.repository'; -export * from './person.repository'; -export * from './search.repository'; -export * from './server-info.repository'; -export * from './shared-link.repository'; -export * from './storage.repository'; -export * from './system-config.repository'; -export * from './system-metadata.repository'; -export * from './tag.repository'; -export * from './user-token.repository'; -export * from './user.repository'; diff --git a/server/src/domain/search/dto/index.ts b/server/src/domain/search/dto/index.ts deleted file mode 100644 index cd914d0ea..000000000 --- a/server/src/domain/search/dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './search.dto'; diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts deleted file mode 100644 index 824a1066c..000000000 --- a/server/src/domain/search/dto/search-suggestion.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional } from '../../domain.util'; - -export enum SearchSuggestionType { - COUNTRY = 'country', - STATE = 'state', - CITY = 'city', - CAMERA_MAKE = 'camera-make', - CAMERA_MODEL = 'camera-model', -} - -export class SearchSuggestionRequestDto { - @IsEnum(SearchSuggestionType) - @IsNotEmpty() - @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) - type!: SearchSuggestionType; - - @IsString() - @Optional() - country?: string; - - @IsString() - @Optional() - state?: string; - - @IsString() - @Optional() - make?: string; - - @IsString() - @Optional() - model?: string; -} diff --git a/server/src/domain/search/index.ts b/server/src/domain/search/index.ts deleted file mode 100644 index 717439d3c..000000000 --- a/server/src/domain/search/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './dto'; -export * from './response-dto'; -export * from './search.service'; diff --git a/server/src/domain/search/response-dto/index.ts b/server/src/domain/search/response-dto/index.ts deleted file mode 100644 index f48856bca..000000000 --- a/server/src/domain/search/response-dto/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './search-explore.response.dto'; -export * from './search-response.dto'; diff --git a/server/src/domain/search/response-dto/search-explore.response.dto.ts b/server/src/domain/search/response-dto/search-explore.response.dto.ts deleted file mode 100644 index 37398d9de..000000000 --- a/server/src/domain/search/response-dto/search-explore.response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AssetResponseDto } from '../../asset'; - -class SearchExploreItem { - value!: string; - data!: AssetResponseDto; -} - -export class SearchExploreResponseDto { - fieldName!: string; - items!: SearchExploreItem[]; -} diff --git a/server/src/domain/search/response-dto/search-response.dto.ts b/server/src/domain/search/response-dto/search-response.dto.ts deleted file mode 100644 index 9dd65e7cc..000000000 --- a/server/src/domain/search/response-dto/search-response.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AlbumResponseDto } from '../../album'; -import { AssetResponseDto } from '../../asset'; - -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer' }) - count!: number; - value!: string; -} - -class SearchFacetResponseDto { - fieldName!: string; - counts!: SearchFacetCountResponseDto[]; -} - -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer' }) - total!: number; - @ApiProperty({ type: 'integer' }) - count!: number; - items!: AlbumResponseDto[]; - facets!: SearchFacetResponseDto[]; -} - -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer' }) - total!: number; - @ApiProperty({ type: 'integer' }) - count!: number; - items!: AssetResponseDto[]; - facets!: SearchFacetResponseDto[]; - nextPage!: string | null; -} - -export class SearchResponseDto { - albums!: SearchAlbumResponseDto; - assets!: SearchAssetResponseDto; -} diff --git a/server/src/domain/server-info/index.ts b/server/src/domain/server-info/index.ts deleted file mode 100644 index 74a46a52b..000000000 --- a/server/src/domain/server-info/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './server-info.dto'; -export * from './server-info.service'; diff --git a/server/src/domain/shared-link/index.ts b/server/src/domain/shared-link/index.ts deleted file mode 100644 index 0b4720850..000000000 --- a/server/src/domain/shared-link/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './shared-link-response.dto'; -export * from './shared-link.dto'; -export * from './shared-link.service'; diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts deleted file mode 100644 index 550ed70ea..000000000 --- a/server/src/domain/shared-link/shared-link.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { SharedLinkType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util'; - -export class SharedLinkCreateDto { - @IsEnum(SharedLinkType) - @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) - type!: SharedLinkType; - - @ValidateUUID({ each: true, optional: true }) - assetIds?: string[]; - - @ValidateUUID({ optional: true }) - albumId?: string; - - @IsString() - @Optional() - description?: string; - - @IsString() - @Optional() - password?: string; - - @ValidateDate({ optional: true, nullable: true }) - expiresAt?: Date | null = null; - - @ValidateBoolean({ optional: true }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true }) - allowDownload?: boolean = true; - - @ValidateBoolean({ optional: true }) - showMetadata?: boolean = true; -} - -export class SharedLinkEditDto { - @Optional() - description?: string; - - @Optional() - password?: string; - - @Optional({ nullable: true }) - expiresAt?: Date | null; - - @Optional() - allowUpload?: boolean; - - @ValidateBoolean({ optional: true }) - allowDownload?: boolean; - - @ValidateBoolean({ optional: true }) - showMetadata?: boolean; - - /** - * Few clients cannot send null to set the expiryTime to never. - * Setting this flag and not sending expiryAt is considered as null instead. - * Clients that can send null values can ignore this. - */ - @ValidateBoolean({ optional: true }) - changeExpiryTime?: boolean; -} - -export class SharedLinkPasswordDto { - @IsString() - @Optional() - @ApiProperty({ example: 'password' }) - password?: string; - - @IsString() - @Optional() - token?: string; -} diff --git a/server/src/domain/smart-info/dto/index.ts b/server/src/domain/smart-info/dto/index.ts deleted file mode 100644 index aa672a787..000000000 --- a/server/src/domain/smart-info/dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './model-config.dto'; diff --git a/server/src/domain/smart-info/index.ts b/server/src/domain/smart-info/index.ts deleted file mode 100644 index a0cbeecf4..000000000 --- a/server/src/domain/smart-info/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './dto'; -export * from './smart-info.service'; diff --git a/server/src/domain/smart-info/smart-info.constant.ts b/server/src/domain/smart-info/smart-info.constant.ts deleted file mode 100644 index 66c31b985..000000000 --- a/server/src/domain/smart-info/smart-info.constant.ts +++ /dev/null @@ -1,129 +0,0 @@ -export type ModelInfo = { - dimSize: number; -}; - -export const CLIP_MODEL_INFO: Record = { - RN50__openai: { - dimSize: 1024, - }, - RN50__yfcc15m: { - dimSize: 1024, - }, - RN50__cc12m: { - dimSize: 1024, - }, - RN101__openai: { - dimSize: 512, - }, - RN101__yfcc15m: { - dimSize: 512, - }, - RN50x4__openai: { - dimSize: 640, - }, - RN50x16__openai: { - dimSize: 768, - }, - RN50x64__openai: { - dimSize: 1024, - }, - 'ViT-B-32__openai': { - dimSize: 512, - }, - 'ViT-B-32__laion2b_e16': { - dimSize: 512, - }, - 'ViT-B-32__laion400m_e31': { - dimSize: 512, - }, - 'ViT-B-32__laion400m_e32': { - dimSize: 512, - }, - 'ViT-B-32__laion2b-s34b-b79k': { - dimSize: 512, - }, - 'ViT-B-16__openai': { - dimSize: 512, - }, - 'ViT-B-16__laion400m_e31': { - dimSize: 512, - }, - 'ViT-B-16__laion400m_e32': { - dimSize: 512, - }, - 'ViT-B-16-plus-240__laion400m_e31': { - dimSize: 640, - }, - 'ViT-B-16-plus-240__laion400m_e32': { - dimSize: 640, - }, - 'ViT-L-14__openai': { - dimSize: 768, - }, - 'ViT-L-14__laion400m_e31': { - dimSize: 768, - }, - 'ViT-L-14__laion400m_e32': { - dimSize: 768, - }, - 'ViT-L-14__laion2b-s32b-b82k': { - dimSize: 768, - }, - 'ViT-L-14-336__openai': { - dimSize: 768, - }, - 'ViT-L-14-quickgelu__dfn2b': { - dimSize: 768, - }, - 'ViT-H-14__laion2b-s32b-b79k': { - dimSize: 1024, - }, - 'ViT-H-14-quickgelu__dfn5b': { - dimSize: 1024, - }, - 'ViT-H-14-378-quickgelu__dfn5b': { - dimSize: 1024, - }, - 'ViT-g-14__laion2b-s12b-b42k': { - dimSize: 1024, - }, - 'LABSE-Vit-L-14': { - dimSize: 768, - }, - 'XLM-Roberta-Large-Vit-B-32': { - dimSize: 512, - }, - 'XLM-Roberta-Large-Vit-B-16Plus': { - dimSize: 640, - }, - 'XLM-Roberta-Large-Vit-L-14': { - dimSize: 768, - }, - 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { - dimSize: 1024, - }, - 'nllb-clip-base-siglip__v1': { - dimSize: 768, - }, - 'nllb-clip-large-siglip__v1': { - dimSize: 1152, - }, -}; - -export function cleanModelName(modelName: string): string { - const token = modelName.split('/').at(-1); - if (!token) { - throw new Error(`Invalid model name: ${modelName}`); - } - - return token.replaceAll(':', '_'); -} - -export function getCLIPModelInfo(modelName: string): ModelInfo { - const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)]; - if (!modelInfo) { - throw new Error(`Unknown CLIP model: ${modelName}`); - } - - return modelInfo; -} diff --git a/server/src/domain/storage-template/index.ts b/server/src/domain/storage-template/index.ts deleted file mode 100644 index f90e36389..000000000 --- a/server/src/domain/storage-template/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './storage-template.service'; diff --git a/server/src/domain/storage/index.ts b/server/src/domain/storage/index.ts deleted file mode 100644 index bdea086bd..000000000 --- a/server/src/domain/storage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './storage.core'; -export * from './storage.service'; diff --git a/server/src/domain/system-config/dto/index.ts b/server/src/domain/system-config/dto/index.ts deleted file mode 100644 index 652e34cc5..000000000 --- a/server/src/domain/system-config/dto/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './system-config-ffmpeg.dto'; -export * from './system-config-library.dto'; -export * from './system-config-oauth.dto'; -export * from './system-config-password-login.dto'; -export * from './system-config-storage-template.dto'; -export * from './system-config-thumbnail.dto'; -export * from './system-config-trash.dto'; -export * from './system-config.dto'; diff --git a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts deleted file mode 100644 index 3a219888f..000000000 --- a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { AudioCodec, CQMode, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - crf!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - threads!: number; - - @IsString() - preset!: string; - - @IsEnum(VideoCodec) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) - targetVideoCodec!: VideoCodec; - - @IsEnum(VideoCodec, { each: true }) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) - acceptedVideoCodecs!: VideoCodec[]; - - @IsEnum(AudioCodec) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) - targetAudioCodec!: AudioCodec; - - @IsEnum(AudioCodec, { each: true }) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) - acceptedAudioCodecs!: AudioCodec[]; - - @IsString() - targetResolution!: string; - - @IsString() - maxBitrate!: string; - - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - bframes!: number; - - @IsInt() - @Min(0) - @Max(6) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - refs!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - gopSize!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - npl!: number; - - @ValidateBoolean() - temporalAQ!: boolean; - - @IsEnum(CQMode) - @ApiProperty({ enumName: 'CQMode', enum: CQMode }) - cqMode!: CQMode; - - @ValidateBoolean() - twoPass!: boolean; - - @IsString() - preferredHwDevice!: string; - - @IsEnum(TranscodePolicy) - @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) - transcode!: TranscodePolicy; - - @IsEnum(TranscodeHWAccel) - @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) - accel!: TranscodeHWAccel; - - @IsEnum(ToneMapping) - @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) - tonemap!: ToneMapping; -} diff --git a/server/src/domain/system-config/dto/system-config-job.dto.ts b/server/src/domain/system-config/dto/system-config-job.dto.ts deleted file mode 100644 index 3307811d7..000000000 --- a/server/src/domain/system-config/dto/system-config-job.dto.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; -import { ConcurrentQueueName, QueueName } from '../../job'; - -export class JobSettingsDto { - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer' }) - concurrency!: number; -} - -export class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SMART_SEARCH]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.MIGRATION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.BACKGROUND_TASK]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SEARCH]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.FACE_DETECTION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SIDECAR]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.LIBRARY]!: JobSettingsDto; -} diff --git a/server/src/domain/system-config/dto/system-config-library.dto.ts b/server/src/domain/system-config/dto/system-config-library.dto.ts deleted file mode 100644 index 85ab62634..000000000 --- a/server/src/domain/system-config/dto/system-config-library.dto.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Type } from 'class-transformer'; -import { - IsNotEmpty, - IsObject, - IsString, - Validate, - ValidateIf, - ValidateNested, - ValidatorConstraint, - ValidatorConstraintInterface, -} from 'class-validator'; -import { ValidateBoolean, validateCronExpression } from '../../domain.util'; - -const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; - -@ValidatorConstraint({ name: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { - return validateCronExpression(expression); - } -} - -export class SystemConfigLibraryScanDto { - @ValidateBoolean() - enabled!: boolean; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @Validate(CronValidator, { message: 'Invalid cron expression' }) - @IsString() - cronExpression!: string; -} - -export class SystemConfigLibraryWatchDto { - @ValidateBoolean() - enabled!: boolean; -} - -export class SystemConfigLibraryDto { - @Type(() => SystemConfigLibraryScanDto) - @ValidateNested() - @IsObject() - scan!: SystemConfigLibraryScanDto; - - @Type(() => SystemConfigLibraryWatchDto) - @ValidateNested() - @IsObject() - watch!: SystemConfigLibraryWatchDto; -} diff --git a/server/src/domain/system-config/dto/system-config-logging.dto.ts b/server/src/domain/system-config/dto/system-config-logging.dto.ts deleted file mode 100644 index 09f78fc86..000000000 --- a/server/src/domain/system-config/dto/system-config-logging.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LogLevel } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigLoggingDto { - @ValidateBoolean() - enabled!: boolean; - - @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) - @IsEnum(LogLevel) - level!: LogLevel; -} diff --git a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts deleted file mode 100644 index 435e68826..000000000 --- a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; -import { CLIPConfig, RecognitionConfig } from '../../smart-info/dto/model-config.dto'; - -export class SystemConfigMachineLearningDto { - @ValidateBoolean() - enabled!: boolean; - - @IsUrl({ require_tld: false, allow_underscores: true }) - @ValidateIf((dto) => dto.enabled) - url!: string; - - @Type(() => CLIPConfig) - @ValidateNested() - @IsObject() - clip!: CLIPConfig; - - @Type(() => RecognitionConfig) - @ValidateNested() - @IsObject() - facialRecognition!: RecognitionConfig; -} diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts deleted file mode 100644 index 9e21e2d5d..000000000 --- a/server/src/domain/system-config/dto/system-config-map.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsString } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigMapDto { - @ValidateBoolean() - enabled!: boolean; - - @IsString() - lightStyle!: string; - - @IsString() - darkStyle!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts deleted file mode 100644 index 379f5643d..000000000 --- a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigNewVersionCheckDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-oauth.dto.ts b/server/src/domain/system-config/dto/system-config-oauth.dto.ts deleted file mode 100644 index 99779bdfe..000000000 --- a/server/src/domain/system-config/dto/system-config-oauth.dto.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -const isEnabled = (config: SystemConfigOAuthDto) => config.enabled; -const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; - -export class SystemConfigOAuthDto { - @ValidateBoolean() - autoLaunch!: boolean; - - @ValidateBoolean() - autoRegister!: boolean; - - @IsString() - buttonText!: string; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - clientId!: string; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - clientSecret!: string; - - @IsNumber() - @Min(0) - defaultStorageQuota!: number; - - @ValidateBoolean() - enabled!: boolean; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - issuerUrl!: string; - - @ValidateBoolean() - mobileOverrideEnabled!: boolean; - - @ValidateIf(isOverrideEnabled) - @IsUrl() - mobileRedirectUri!: string; - - @IsString() - scope!: string; - - @IsString() - @IsNotEmpty() - signingAlgorithm!: string; - - @IsString() - storageLabelClaim!: string; - - @IsString() - storageQuotaClaim!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-password-login.dto.ts b/server/src/domain/system-config/dto/system-config-password-login.dto.ts deleted file mode 100644 index 279bcc5a6..000000000 --- a/server/src/domain/system-config/dto/system-config-password-login.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigPasswordLoginDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts deleted file mode 100644 index 11e0ae289..000000000 --- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigReverseGeocodingDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-server.dto.ts b/server/src/domain/system-config/dto/system-config-server.dto.ts deleted file mode 100644 index 83a2b0df9..000000000 --- a/server/src/domain/system-config/dto/system-config-server.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsString } from 'class-validator'; - -export class SystemConfigServerDto { - @IsString() - externalDomain!: string; - - @IsString() - loginPageMessage!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts b/server/src/domain/system-config/dto/system-config-storage-template.dto.ts deleted file mode 100644 index 615fd8521..000000000 --- a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigStorageTemplateDto { - @ValidateBoolean() - enabled!: boolean; - - @ValidateBoolean() - hashVerificationEnabled!: boolean; - - @IsNotEmpty() - @IsString() - template!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-theme.dto.ts b/server/src/domain/system-config/dto/system-config-theme.dto.ts deleted file mode 100644 index f47b51e0e..000000000 --- a/server/src/domain/system-config/dto/system-config-theme.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsString } from 'class-validator'; - -export class SystemConfigThemeDto { - @IsString() - customCss!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts b/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts deleted file mode 100644 index c389ef77a..000000000 --- a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Colorspace } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsEnum, IsInt, Max, Min } from 'class-validator'; - -export class SystemConfigThumbnailDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - webpSize!: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - jpegSize!: number; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - quality!: number; - - @IsEnum(Colorspace) - @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) - colorspace!: Colorspace; -} diff --git a/server/src/domain/system-config/dto/system-config-trash.dto.ts b/server/src/domain/system-config/dto/system-config-trash.dto.ts deleted file mode 100644 index 482410703..000000000 --- a/server/src/domain/system-config/dto/system-config-trash.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Min } from 'class-validator'; -import { ValidateBoolean } from '../../domain.util'; - -export class SystemConfigTrashDto { - @ValidateBoolean() - enabled!: boolean; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - days!: number; -} diff --git a/server/src/domain/system-config/dto/system-config-user.dto.ts b/server/src/domain/system-config/dto/system-config-user.dto.ts deleted file mode 100644 index 22d6ef5fc..000000000 --- a/server/src/domain/system-config/dto/system-config-user.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Min } from 'class-validator'; - -export class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - deleteDelay!: number; -} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts deleted file mode 100644 index 4906e293e..000000000 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { SystemConfig } from '@app/infra/entities'; -import { Type } from 'class-transformer'; -import { IsObject, ValidateNested } from 'class-validator'; -import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; -import { SystemConfigJobDto } from './system-config-job.dto'; -import { SystemConfigLibraryDto } from './system-config-library.dto'; -import { SystemConfigLoggingDto } from './system-config-logging.dto'; -import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto'; -import { SystemConfigMapDto } from './system-config-map.dto'; -import { SystemConfigNewVersionCheckDto } from './system-config-new-version-check.dto'; -import { SystemConfigOAuthDto } from './system-config-oauth.dto'; -import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; -import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto'; -import { SystemConfigServerDto } from './system-config-server.dto'; -import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; -import { SystemConfigThemeDto } from './system-config-theme.dto'; -import { SystemConfigThumbnailDto } from './system-config-thumbnail.dto'; -import { SystemConfigTrashDto } from './system-config-trash.dto'; -import { SystemConfigUserDto } from './system-config-user.dto'; - -export class SystemConfigDto implements SystemConfig { - @Type(() => SystemConfigFFmpegDto) - @ValidateNested() - @IsObject() - ffmpeg!: SystemConfigFFmpegDto; - - @Type(() => SystemConfigLoggingDto) - @ValidateNested() - @IsObject() - logging!: SystemConfigLoggingDto; - - @Type(() => SystemConfigMachineLearningDto) - @ValidateNested() - @IsObject() - machineLearning!: SystemConfigMachineLearningDto; - - @Type(() => SystemConfigMapDto) - @ValidateNested() - @IsObject() - map!: SystemConfigMapDto; - - @Type(() => SystemConfigNewVersionCheckDto) - @ValidateNested() - @IsObject() - newVersionCheck!: SystemConfigNewVersionCheckDto; - - @Type(() => SystemConfigOAuthDto) - @ValidateNested() - @IsObject() - oauth!: SystemConfigOAuthDto; - - @Type(() => SystemConfigPasswordLoginDto) - @ValidateNested() - @IsObject() - passwordLogin!: SystemConfigPasswordLoginDto; - - @Type(() => SystemConfigReverseGeocodingDto) - @ValidateNested() - @IsObject() - reverseGeocoding!: SystemConfigReverseGeocodingDto; - - @Type(() => SystemConfigStorageTemplateDto) - @ValidateNested() - @IsObject() - storageTemplate!: SystemConfigStorageTemplateDto; - - @Type(() => SystemConfigJobDto) - @ValidateNested() - @IsObject() - job!: SystemConfigJobDto; - - @Type(() => SystemConfigThumbnailDto) - @ValidateNested() - @IsObject() - thumbnail!: SystemConfigThumbnailDto; - - @Type(() => SystemConfigTrashDto) - @ValidateNested() - @IsObject() - trash!: SystemConfigTrashDto; - - @Type(() => SystemConfigThemeDto) - @ValidateNested() - @IsObject() - theme!: SystemConfigThemeDto; - - @Type(() => SystemConfigLibraryDto) - @ValidateNested() - @IsObject() - library!: SystemConfigLibraryDto; - - @Type(() => SystemConfigServerDto) - @ValidateNested() - @IsObject() - server!: SystemConfigServerDto; - - @Type(() => SystemConfigUserDto) - @ValidateNested() - @IsObject() - user!: SystemConfigUserDto; -} - -export function mapConfig(config: SystemConfig): SystemConfigDto { - return config; -} diff --git a/server/src/domain/system-config/index.ts b/server/src/domain/system-config/index.ts deleted file mode 100644 index fb71613dd..000000000 --- a/server/src/domain/system-config/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './dto'; -export * from './response-dto'; -export * from './system-config.constants'; -export * from './system-config.core'; -export * from './system-config.service'; diff --git a/server/src/domain/system-config/response-dto/index.ts b/server/src/domain/system-config/response-dto/index.ts deleted file mode 100644 index 9cb60bece..000000000 --- a/server/src/domain/system-config/response-dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './system-config-template-storage-option.dto'; diff --git a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts b/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts deleted file mode 100644 index f0c8b9b64..000000000 --- a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class SystemConfigTemplateStorageOptionDto { - yearOptions!: string[]; - monthOptions!: string[]; - weekOptions!: string[]; - dayOptions!: string[]; - hourOptions!: string[]; - minuteOptions!: string[]; - secondOptions!: string[]; - presetOptions!: string[]; -} diff --git a/server/src/domain/system-config/system-config-map-theme.dto.ts b/server/src/domain/system-config/system-config-map-theme.dto.ts deleted file mode 100644 index 9286d8d23..000000000 --- a/server/src/domain/system-config/system-config-map-theme.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; - -export enum MapTheme { - LIGHT = 'light', - DARK = 'dark', -} - -export class MapThemeDto { - @IsEnum(MapTheme) - @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) - theme!: MapTheme; -} diff --git a/server/src/domain/system-config/system-config.constants.ts b/server/src/domain/system-config/system-config.constants.ts deleted file mode 100644 index 0290472aa..000000000 --- a/server/src/domain/system-config/system-config.constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const supportedYearTokens = ['y', 'yy']; -export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM']; -export const supportedWeekTokens = ['W', 'WW']; -export const supportedDayTokens = ['d', 'dd']; -export const supportedHourTokens = ['h', 'hh', 'H', 'HH']; -export const supportedMinuteTokens = ['m', 'mm']; -export const supportedSecondTokens = ['s', 'ss', 'SSS']; -export const supportedPresetTokens = [ - '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', - '{{y}}/{{MM}}-{{dd}}/{{filename}}', - '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', - '{{y}}/{{MM}}/{{filename}}', - '{{y}}/{{MMM}}/{{filename}}', - '{{y}}/{{MMMM}}/{{filename}}', - '{{y}}/{{MM}}/{{dd}}/{{filename}}', - '{{y}}/{{MMMM}}/{{dd}}/{{filename}}', - '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', - '{{y}}-{{MM}}-{{dd}}/{{filename}}', - '{{y}}-{{MMM}}-{{dd}}/{{filename}}', - '{{y}}-{{MMMM}}-{{dd}}/{{filename}}', - '{{y}}/{{y}}-{{MM}}/{{filename}}', - '{{y}}/{{y}}-{{WW}}/{{filename}}', - '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}', - '{{y}}/{{y}}-{{MM}}/{{assetId}}', - '{{y}}/{{y}}-{{WW}}/{{assetId}}', - '{{album}}/{{filename}}', -]; diff --git a/server/src/domain/tag/index.ts b/server/src/domain/tag/index.ts deleted file mode 100644 index 38e9b389f..000000000 --- a/server/src/domain/tag/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './tag-response.dto'; -export * from './tag.dto'; -export * from './tag.service'; diff --git a/server/src/domain/tag/tag-response.dto.ts b/server/src/domain/tag/tag-response.dto.ts deleted file mode 100644 index a533b15c9..000000000 --- a/server/src/domain/tag/tag-response.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TagEntity, TagType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; - -export class TagResponseDto { - id!: string; - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: string; - name!: string; - userId!: string; -} - -export function mapTag(entity: TagEntity): TagResponseDto { - return { - id: entity.id, - type: entity.type, - name: entity.name, - userId: entity.userId, - }; -} diff --git a/server/src/domain/tag/tag.dto.ts b/server/src/domain/tag/tag.dto.ts deleted file mode 100644 index 900aac9bd..000000000 --- a/server/src/domain/tag/tag.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TagType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional } from '../domain.util'; - -export class CreateTagDto { - @IsString() - @IsNotEmpty() - name!: string; - - @IsEnum(TagType) - @IsNotEmpty() - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: TagType; -} - -export class UpdateTagDto { - @IsString() - @Optional() - name?: string; -} diff --git a/server/src/domain/trash/index.ts b/server/src/domain/trash/index.ts deleted file mode 100644 index 3cd00e191..000000000 --- a/server/src/domain/trash/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './trash.service'; diff --git a/server/src/domain/user/dto/create-profile-image.dto.ts b/server/src/domain/user/dto/create-profile-image.dto.ts deleted file mode 100644 index c7a1dc68b..000000000 --- a/server/src/domain/user/dto/create-profile-image.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { UploadFieldName } from '../../asset/asset.service'; - -export class CreateProfileImageDto { - @ApiProperty({ type: 'string', format: 'binary' }) - [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; -} diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts deleted file mode 100644 index f0cc7938c..000000000 --- a/server/src/domain/user/dto/create-user.dto.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util'; - -export class CreateUserDto { - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email!: string; - - @IsNotEmpty() - @IsString() - password!: string; - - @IsNotEmpty() - @IsString() - name!: string; - - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional({ nullable: true }) - @IsNumber() - @IsPositive() - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true }) - shouldChangePassword?: boolean; -} - -export class CreateAdminDto { - @IsNotEmpty() - isAdmin!: true; - - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - email!: string; - - @IsNotEmpty() - password!: string; - - @IsNotEmpty() - name!: string; -} - -export class CreateUserOAuthDto { - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - email!: string; - - @IsNotEmpty() - oauthId!: string; - - name?: string; -} diff --git a/server/src/domain/user/dto/delete-user.dto.ts b/server/src/domain/user/dto/delete-user.dto.ts deleted file mode 100644 index 88f55f4af..000000000 --- a/server/src/domain/user/dto/delete-user.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from '../../domain.util'; - -export class DeleteUserDto { - @ValidateBoolean({ optional: true }) - force?: boolean; -} diff --git a/server/src/domain/user/dto/index.ts b/server/src/domain/user/dto/index.ts deleted file mode 100644 index 2d166de36..000000000 --- a/server/src/domain/user/dto/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-profile-image.dto'; -export * from './create-user.dto'; -export * from './delete-user.dto'; -export * from './update-user.dto'; diff --git a/server/src/domain/user/dto/update-user.dto.spec.ts b/server/src/domain/user/dto/update-user.dto.spec.ts deleted file mode 100644 index 8e9013f29..000000000 --- a/server/src/domain/user/dto/update-user.dto.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UpdateUserDto } from './update-user.dto'; - -describe('update user DTO', () => { - it('should allow emails without a tld', async () => { - const someEmail = 'test@test'; - - const dto = plainToInstance(UpdateUserDto, { - email: someEmail, - id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', - }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); - }); -}); diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts deleted file mode 100644 index e8cce2214..000000000 --- a/server/src/domain/user/dto/update-user.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { UserAvatarColor } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; -import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util'; - -export class UpdateUserDto { - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @Optional() - @IsString() - @Transform(toSanitized) - storageLabel?: string; - - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; - - @ValidateBoolean({ optional: true }) - isAdmin?: boolean; - - @ValidateBoolean({ optional: true }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor?: UserAvatarColor; - - @Optional({ nullable: true }) - @IsNumber() - @IsPositive() - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes?: number | null; -} diff --git a/server/src/domain/user/index.ts b/server/src/domain/user/index.ts deleted file mode 100644 index 724859197..000000000 --- a/server/src/domain/user/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './dto'; -export * from './response-dto'; -export * from './user.core'; -export * from './user.service'; diff --git a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts b/server/src/domain/user/response-dto/create-profile-image-response.dto.ts deleted file mode 100644 index 2c7fd17be..000000000 --- a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class CreateProfileImageResponseDto { - userId!: string; - profileImagePath!: string; -} - -export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { - return { - userId: userId, - profileImagePath: profileImagePath, - }; -} diff --git a/server/src/domain/user/response-dto/index.ts b/server/src/domain/user/response-dto/index.ts deleted file mode 100644 index 8c550a4ff..000000000 --- a/server/src/domain/user/response-dto/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './create-profile-image-response.dto'; -export * from './user-response.dto'; diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts deleted file mode 100644 index bd437ea34..000000000 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { UserAvatarColor, UserEntity, UserStatus } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; - -export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { - const values = Object.values(UserAvatarColor); - const randomIndex = Math.floor( - [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, - ); - return values[randomIndex] as UserAvatarColor; -}; - -export class UserDto { - id!: string; - name!: string; - email!: string; - profileImagePath!: string; - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor!: UserAvatarColor; -} - -export class UserResponseDto extends UserDto { - storageLabel!: string | null; - shouldChangePassword!: boolean; - isAdmin!: boolean; - createdAt!: Date; - deletedAt!: Date | null; - updatedAt!: Date; - oauthId!: string; - memoriesEnabled?: boolean; - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaUsageInBytes!: number | null; - @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) - status!: string; -} - -export const mapSimpleUser = (entity: UserEntity): UserDto => { - return { - id: entity.id, - email: entity.email, - name: entity.name, - profileImagePath: entity.profileImagePath, - avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), - }; -}; - -export function mapUser(entity: UserEntity): UserResponseDto { - return { - ...mapSimpleUser(entity), - storageLabel: entity.storageLabel, - shouldChangePassword: entity.shouldChangePassword, - isAdmin: entity.isAdmin, - createdAt: entity.createdAt, - deletedAt: entity.deletedAt, - updatedAt: entity.updatedAt, - oauthId: entity.oauthId, - memoriesEnabled: entity.memoriesEnabled, - quotaSizeInBytes: entity.quotaSizeInBytes, - quotaUsageInBytes: entity.quotaUsageInBytes, - status: entity.status, - }; -} diff --git a/server/src/domain/activity/activity.dto.ts b/server/src/dtos/activity.dto.ts similarity index 90% rename from server/src/domain/activity/activity.dto.ts rename to server/src/dtos/activity.dto.ts index a5a5bd3df..bd0d40095 100644 --- a/server/src/domain/activity/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,8 +1,8 @@ -import { ActivityEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; -import { Optional, ValidateUUID } from '../domain.util'; -import { UserDto, mapSimpleUser } from '../user/response-dto'; +import { UserDto, mapSimpleUser } from 'src/dtos/user.dto'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { Optional, ValidateUUID } from 'src/validation'; export enum ReactionType { COMMENT = 'comment', diff --git a/server/src/domain/album/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts similarity index 83% rename from server/src/domain/album/album-response.dto.spec.ts rename to server/src/dtos/album-response.dto.spec.ts index c8485af65..2a6d59abf 100644 --- a/server/src/domain/album/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -1,5 +1,5 @@ -import { albumStub } from '@test'; -import { mapAlbum } from './album-response.dto'; +import { mapAlbum } from 'src/dtos/album.dto'; +import { albumStub } from 'test/fixtures/album.stub'; describe('mapAlbum', () => { it('should set start and end dates', () => { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/dtos/album.dto.ts similarity index 58% rename from server/src/domain/album/album-response.dto.ts rename to server/src/dtos/album.dto.ts index bcca1cd31..3f7af0f53 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,9 +1,87 @@ -import { AlbumEntity, AssetOrder } from '@app/infra/entities'; -import { Optional } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; -import { AssetResponseDto, mapAsset } from '../asset'; -import { AuthDto } from '../auth/auth.dto'; -import { UserResponseDto, mapUser } from '../user'; +import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; + +export class AlbumInfoDto { + @ValidateBoolean({ optional: true }) + withoutAssets?: boolean; +} + +export class AddUsersDto { + @ValidateUUID({ each: true }) + @ArrayNotEmpty() + sharedUserIds!: string[]; +} + +export class CreateAlbumDto { + @IsString() + @ApiProperty() + albumName!: string; + + @IsString() + @Optional() + description?: string; + + @ValidateUUID({ optional: true, each: true }) + sharedWithUserIds?: string[]; + + @ValidateUUID({ optional: true, each: true }) + assetIds?: string[]; +} + +export class UpdateAlbumDto { + @Optional() + @IsString() + albumName?: string; + + @Optional() + @IsString() + description?: string; + + @ValidateUUID({ optional: true }) + albumThumbnailAssetId?: string; + + @ValidateBoolean({ optional: true }) + isActivityEnabled?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; +} + +export class GetAlbumsDto { + @ValidateBoolean({ optional: true }) + /** + * true: only shared albums + * false: only non-shared own albums + * undefined: shared and owned albums + */ + shared?: boolean; + + /** + * Only returns albums that contain the asset + * Ignores the shared parameter + * undefined: get all albums + */ + @ValidateUUID({ optional: true }) + assetId?: string; +} + +export class AlbumCountResponseDto { + @ApiProperty({ type: 'integer' }) + owned!: number; + + @ApiProperty({ type: 'integer' }) + shared!: number; + + @ApiProperty({ type: 'integer' }) + notShared!: number; +} export class AlbumResponseDto { id!: string; @@ -73,14 +151,3 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true); export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false); - -export class AlbumCountResponseDto { - @ApiProperty({ type: 'integer' }) - owned!: number; - - @ApiProperty({ type: 'integer' }) - shared!: number; - - @ApiProperty({ type: 'integer' }) - notShared!: number; -} diff --git a/server/src/domain/api-key/api-key.dto.ts b/server/src/dtos/api-key.dto.ts similarity index 90% rename from server/src/domain/api-key/api-key.dto.ts rename to server/src/dtos/api-key.dto.ts index c25ef5fd4..1f4f85521 100644 --- a/server/src/domain/api-key/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty, IsString } from 'class-validator'; -import { Optional } from '../domain.util'; +import { Optional } from 'src/validation'; export class APIKeyCreateDto { @IsString() @IsNotEmpty() diff --git a/server/src/domain/asset/response-dto/asset-ids-response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts similarity index 92% rename from server/src/domain/asset/response-dto/asset-ids-response.dto.ts rename to server/src/dtos/asset-ids.response.dto.ts index 9bb6a5b36..fdc9942e3 100644 --- a/server/src/domain/asset/response-dto/asset-ids-response.dto.ts +++ b/server/src/dtos/asset-ids.response.dto.ts @@ -1,4 +1,4 @@ -import { ValidateUUID } from '../../domain.util'; +import { ValidateUUID } from 'src/validation'; /** @deprecated Use `BulkIdResponseDto` instead */ export enum AssetIdErrorReason { diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts similarity index 82% rename from server/src/domain/asset/response-dto/asset-response.dto.ts rename to server/src/dtos/asset-response.dto.ts index 2961a9dcc..bdda36d15 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,11 +1,12 @@ -import { AuthDto } from '@app/domain/auth/auth.dto'; -import { AssetEntity, AssetFaceEntity, AssetType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from '../../person/person.dto'; -import { TagResponseDto, mapTag } from '../../tag'; -import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; -import { ExifResponseDto, mapExif } from './exif-response.dto'; -import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto'; +import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; export class SanitizedAssetResponseDto { id!: string; @@ -81,7 +82,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.resizePath, + resized: !!entity.previewPath, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -99,7 +100,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, originalPath: entity.originalPath, originalFileName: entity.originalFileName, - resized: !!entity.resizePath, + resized: !!entity.previewPath, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, @@ -130,6 +131,23 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As } export class MemoryLaneResponseDto { + @ApiProperty({ deprecated: true }) title!: string; + + @ApiProperty({ type: 'integer' }) + yearsAgo!: number; + assets!: AssetResponseDto[]; } + +export class SmartInfoResponseDto { + tags?: string[] | null; + objects?: string[] | null; +} + +export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto { + return { + tags: entity.tags, + objects: entity.objects, + }; +} diff --git a/server/src/dtos/asset-v1-response.dto.ts b/server/src/dtos/asset-v1-response.dto.ts new file mode 100644 index 000000000..4b1e97b47 --- /dev/null +++ b/server/src/dtos/asset-v1-response.dto.ts @@ -0,0 +1,45 @@ +export class AssetBulkUploadCheckResult { + id!: string; + action!: AssetUploadAction; + reason?: AssetRejectReason; + assetId?: string; +} + +export class AssetBulkUploadCheckResponseDto { + results!: AssetBulkUploadCheckResult[]; +} + +export enum AssetUploadAction { + ACCEPT = 'accept', + REJECT = 'reject', +} + +export enum AssetRejectReason { + DUPLICATE = 'duplicate', + UNSUPPORTED_FORMAT = 'unsupported-format', +} + +export class AssetFileUploadResponseDto { + id!: string; + duplicate!: boolean; +} + +export class CheckExistingAssetsResponseDto { + existingIds!: string[]; +} + +export class CuratedLocationsResponseDto { + id!: string; + city!: string; + resizePath!: string; + deviceAssetId!: string; + deviceId!: string; +} + +export class CuratedObjectsResponseDto { + id!: string; + object!: string; + resizePath!: string; + deviceAssetId!: string; + deviceId!: string; +} diff --git a/server/src/dtos/asset-v1.dto.ts b/server/src/dtos/asset-v1.dto.ts new file mode 100644 index 000000000..50ff3d18b --- /dev/null +++ b/server/src/dtos/asset-v1.dto.ts @@ -0,0 +1,154 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { UploadFieldName } from 'src/dtos/asset.dto'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; + +export class AssetBulkUploadCheckItem { + @IsString() + @IsNotEmpty() + id!: string; + + /** base64 or hex encoded sha1 hash */ + @IsString() + @IsNotEmpty() + checksum!: string; +} + +export class AssetBulkUploadCheckDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetBulkUploadCheckItem) + assets!: AssetBulkUploadCheckItem[]; +} + +export class AssetSearchDto { + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @Optional() + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + skip?: number; + + @Optional() + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + take?: number; + + @Optional() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + userId?: string; + + @ValidateDate({ optional: true }) + updatedAfter?: Date; + + @ValidateDate({ optional: true }) + updatedBefore?: Date; +} + +export class CheckExistingAssetsDto { + @ArrayNotEmpty() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + deviceAssetIds!: string[]; + + @IsNotEmpty() + deviceId!: string; +} + +export class CreateAssetDto { + @ValidateUUID({ optional: true }) + libraryId?: string; + + @IsNotEmpty() + @IsString() + deviceAssetId!: string; + + @IsNotEmpty() + @IsString() + deviceId!: string; + + @ValidateDate() + fileCreatedAt!: Date; + + @ValidateDate() + fileModifiedAt!: Date; + + @Optional() + @IsString() + duration?: string; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isVisible?: boolean; + + @ValidateBoolean({ optional: true }) + isOffline?: boolean; + + @ValidateBoolean({ optional: true }) + isReadOnly?: boolean; + + // The properties below are added to correctly generate the API docs + // and client SDKs. Validation should be handled in the controller. + @ApiProperty({ type: 'string', format: 'binary' }) + [UploadFieldName.ASSET_DATA]!: any; + + @ApiProperty({ type: 'string', format: 'binary', required: false }) + [UploadFieldName.LIVE_PHOTO_DATA]?: any; + + @ApiProperty({ type: 'string', format: 'binary', required: false }) + [UploadFieldName.SIDECAR_DATA]?: any; +} + +export enum GetAssetThumbnailFormatEnum { + JPEG = 'JPEG', + WEBP = 'WEBP', +} + +export class GetAssetThumbnailDto { + @Optional() + @IsEnum(GetAssetThumbnailFormatEnum) + @ApiProperty({ + type: String, + enum: GetAssetThumbnailFormatEnum, + default: GetAssetThumbnailFormatEnum.WEBP, + required: false, + enumName: 'ThumbnailFormat', + }) + format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP; +} + +export class SearchPropertiesDto { + tags?: string[]; + objects?: string[]; + assetType?: string; + orientation?: string; + lensModel?: string; + make?: string; + model?: string; + city?: string; + state?: string; + country?: string; +} + +export class ServeFileDto { + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is serve thumbnail (resize) file' }) + isThumb?: boolean; + + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is request made from web' }) + isWeb?: boolean; +} diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts new file mode 100644 index 000000000..72f1b24c1 --- /dev/null +++ b/server/src/dtos/asset.dto.ts @@ -0,0 +1,132 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsDateString, + IsEnum, + IsInt, + IsLatitude, + IsLongitude, + IsNotEmpty, + IsPositive, + IsString, + ValidateIf, +} from 'class-validator'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { AssetStats } from 'src/interfaces/asset.interface'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; + +export class DeviceIdDto { + @IsNotEmpty() + @IsString() + deviceId!: string; +} + +const hasGPS = (o: { latitude: undefined; longitude: undefined }) => + o.latitude !== undefined || o.longitude !== undefined; +const ValidateGPS = () => ValidateIf(hasGPS); + +export class UpdateAssetBase { + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @Optional() + @IsDateString() + dateTimeOriginal?: string; + + @ValidateGPS() + @IsLatitude() + @IsNotEmpty() + latitude?: number; + + @ValidateGPS() + @IsLongitude() + @IsNotEmpty() + longitude?: number; +} + +export class AssetBulkUpdateDto extends UpdateAssetBase { + @ValidateUUID({ each: true }) + ids!: string[]; + + @ValidateUUID({ optional: true }) + stackParentId?: string; + + @ValidateBoolean({ optional: true }) + removeParent?: boolean; +} + +export class UpdateAssetDto extends UpdateAssetBase { + @Optional() + @IsString() + description?: string; +} + +export class RandomAssetsDto { + @Optional() + @IsInt() + @IsPositive() + @Type(() => Number) + count?: number; +} + +export class AssetBulkDeleteDto extends BulkIdsDto { + @ValidateBoolean({ optional: true }) + force?: boolean; +} + +export class AssetIdsDto { + @ValidateUUID({ each: true }) + assetIds!: string[]; +} + +export enum AssetJobName { + REGENERATE_THUMBNAIL = 'regenerate-thumbnail', + REFRESH_METADATA = 'refresh-metadata', + TRANSCODE_VIDEO = 'transcode-video', +} + +export class AssetJobsDto extends AssetIdsDto { + @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) + @IsEnum(AssetJobName) + name!: AssetJobName; +} + +export class AssetStatsDto { + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isTrashed?: boolean; +} + +export class AssetStatsResponseDto { + @ApiProperty({ type: 'integer' }) + images!: number; + + @ApiProperty({ type: 'integer' }) + videos!: number; + + @ApiProperty({ type: 'integer' }) + total!: number; +} + +export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { + return { + images: stats[AssetType.IMAGE], + videos: stats[AssetType.VIDEO], + total: Object.values(stats).reduce((total, value) => total + value, 0), + }; +}; +export enum UploadFieldName { + ASSET_DATA = 'assetData', + LIVE_PHOTO_DATA = 'livePhotoData', + SIDECAR_DATA = 'sidecarData', + PROFILE_DATA = 'file', +} diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/dtos/audit.dto.ts similarity index 87% rename from server/src/domain/audit/audit.dto.ts rename to server/src/dtos/audit.dto.ts index 0f3f04dab..e83efca76 100644 --- a/server/src/domain/audit/audit.dto.ts +++ b/server/src/dtos/audit.dto.ts @@ -1,8 +1,9 @@ -import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { Optional, ValidateDate, ValidateUUID } from '../domain.util'; +import { EntityType } from 'src/entities/audit.entity'; +import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/dtos/auth.dto.ts similarity index 90% rename from server/src/domain/auth/auth.dto.ts rename to server/src/dtos/auth.dto.ts index 2f6f4b4b7..f3f2270d0 100644 --- a/server/src/domain/auth/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,7 +1,10 @@ -import { APIKeyEntity, SharedLinkEntity, UserEntity, UserTokenEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { UserEntity } from 'src/entities/user.entity'; export class AuthDto { user!: UserEntity; diff --git a/server/src/domain/download/download.dto.ts b/server/src/dtos/download.dto.ts similarity index 92% rename from server/src/domain/download/download.dto.ts rename to server/src/dtos/download.dto.ts index 3785a9d43..e6588a994 100644 --- a/server/src/domain/download/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsPositive } from 'class-validator'; -import { Optional, ValidateUUID } from '../domain.util'; +import { Optional, ValidateUUID } from 'src/validation'; export class DownloadInfoDto { @ValidateUUID({ each: true, optional: true }) diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/dtos/exif.dto.ts similarity index 97% rename from server/src/domain/asset/response-dto/exif-response.dto.ts rename to server/src/dtos/exif.dto.ts index f4d0226b4..6724de98f 100644 --- a/server/src/domain/asset/response-dto/exif-response.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,5 +1,5 @@ -import { ExifEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; +import { ExifEntity } from 'src/entities/exif.entity'; export class ExifResponseDto { make?: string | null = null; diff --git a/server/src/domain/job/job.dto.ts b/server/src/dtos/job.dto.ts similarity index 94% rename from server/src/domain/job/job.dto.ts rename to server/src/dtos/job.dto.ts index 87be1332f..1173ad8d6 100644 --- a/server/src/domain/job/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty } from 'class-validator'; -import { ValidateBoolean } from '../domain.util'; -import { JobCommand, QueueName } from './job.constants'; +import { JobCommand, QueueName } from 'src/interfaces/job.interface'; +import { ValidateBoolean } from 'src/validation'; export class JobIdParamDto { @IsNotEmpty() diff --git a/server/src/domain/library/library.dto.ts b/server/src/dtos/library.dto.ts similarity index 95% rename from server/src/domain/library/library.dto.ts rename to server/src/dtos/library.dto.ts index fcce02f87..951012a85 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,7 +1,7 @@ -import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateUUID } from '../domain.util'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class CreateLibraryDto { @IsEnum(LibraryType) diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts new file mode 100644 index 000000000..ecd62785f --- /dev/null +++ b/server/src/dtos/memory.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; + +class MemoryBaseDto { + @ValidateBoolean({ optional: true }) + isSaved?: boolean; + + @ValidateDate({ optional: true }) + seenAt?: Date; +} + +class OnThisDayDto { + @IsInt() + @IsPositive() + year!: number; +} + +type MemoryData = OnThisDayDto; + +export class MemoryUpdateDto extends MemoryBaseDto { + @ValidateDate({ optional: true }) + memoryAt?: Date; +} + +export class MemoryCreateDto extends MemoryBaseDto { + @IsEnum(MemoryType) + @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + type!: MemoryType; + + @IsObject() + @ValidateNested() + @Type((options) => { + switch (options?.object.type) { + case MemoryType.ON_THIS_DAY: { + return OnThisDayDto; + } + + default: { + return Object; + } + } + }) + data!: MemoryData; + + @ValidateDate() + memoryAt!: Date; + + @ValidateUUID({ optional: true, each: true }) + assetIds?: string[]; +} + +export class MemoryResponseDto { + id!: string; + createdAt!: Date; + updatedAt!: Date; + deletedAt?: Date; + memoryAt!: Date; + seenAt?: Date; + ownerId!: string; + type!: MemoryType; + data!: MemoryData; + isSaved!: boolean; + assets!: AssetResponseDto[]; +} + +export const mapMemory = (entity: MemoryEntity): MemoryResponseDto => { + return { + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + deletedAt: entity.deletedAt, + memoryAt: entity.memoryAt, + seenAt: entity.seenAt, + ownerId: entity.ownerId, + type: entity.type, + data: entity.data, + isSaved: entity.isSaved, + assets: entity.assets.map((asset) => mapAsset(asset)), + }; +}; diff --git a/server/src/domain/smart-info/dto/model-config.dto.ts b/server/src/dtos/model-config.dto.ts similarity index 87% rename from server/src/domain/smart-info/dto/model-config.dto.ts rename to server/src/dtos/model-config.dto.ts index b9e27669f..d1e8bf339 100644 --- a/server/src/domain/smart-info/dto/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { Optional, ValidateBoolean } from '../../domain.util'; -import { CLIPMode, ModelType } from '../../repositories'; +import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.interface'; +import { Optional, ValidateBoolean } from 'src/validation'; export class ModelConfig { @ValidateBoolean() diff --git a/server/src/domain/partner/partner.dto.ts b/server/src/dtos/partner.dto.ts similarity index 79% rename from server/src/domain/partner/partner.dto.ts rename to server/src/dtos/partner.dto.ts index 17afcad5d..187f8f341 100644 --- a/server/src/domain/partner/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty } from 'class-validator'; -import { UserResponseDto } from '../user'; +import { UserResponseDto } from 'src/dtos/user.dto'; export class UpdatePartnerDto { @IsNotEmpty() diff --git a/server/src/domain/person/person.dto.ts b/server/src/dtos/person.dto.ts similarity index 94% rename from server/src/domain/person/person.dto.ts rename to server/src/dtos/person.dto.ts index a00971c6b..b28f18603 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,9 +1,10 @@ -import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator'; -import { AuthDto } from '../auth'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export class PersonCreateDto { /** diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/dtos/search.dto.ts similarity index 61% rename from server/src/domain/search/dto/search.dto.ts rename to server/src/dtos/search.dto.ts index 1bc67266a..d96ce0d98 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,8 +1,12 @@ -import { AssetOrder, AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../../domain.util'; +import { AlbumResponseDto } from 'src/dtos/album.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetOrder } from 'src/entities/album.entity'; +import { AssetType } from 'src/entities/asset.entity'; +import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; class BaseSearchDto { @ValidateUUID({ optional: true }) @@ -159,13 +163,25 @@ export class MetadataSearchDto extends BaseSearchDto { @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) resizePath?: string; @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) webpPath?: string; + @IsString() + @IsNotEmpty() + @Optional() + previewPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + thumbnailPath?: string; + @IsString() @IsNotEmpty() @Optional() @@ -262,3 +278,130 @@ export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto { admin2name: place.admin2Name, }; } +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', +} + +export class SearchSuggestionRequestDto { + @IsEnum(SearchSuggestionType) + @IsNotEmpty() + @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) + type!: SearchSuggestionType; + + @IsString() + @Optional() + country?: string; + + @IsString() + @Optional() + state?: string; + + @IsString() + @Optional() + make?: string; + + @IsString() + @Optional() + model?: string; +} + +class SearchFacetCountResponseDto { + @ApiProperty({ type: 'integer' }) + count!: number; + value!: string; +} + +class SearchFacetResponseDto { + fieldName!: string; + counts!: SearchFacetCountResponseDto[]; +} + +class SearchAlbumResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; + @ApiProperty({ type: 'integer' }) + count!: number; + items!: AlbumResponseDto[]; + facets!: SearchFacetResponseDto[]; +} + +class SearchAssetResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; + @ApiProperty({ type: 'integer' }) + count!: number; + items!: AssetResponseDto[]; + facets!: SearchFacetResponseDto[]; + nextPage!: string | null; +} + +export class SearchResponseDto { + albums!: SearchAlbumResponseDto; + assets!: SearchAssetResponseDto; +} + +class SearchExploreItem { + value!: string; + data!: AssetResponseDto; +} + +export class SearchExploreResponseDto { + fieldName!: string; + items!: SearchExploreItem[]; +} + +export class MapMarkerDto { + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateDate({ optional: true }) + fileCreatedAfter?: Date; + + @ValidateDate({ optional: true }) + fileCreatedBefore?: Date; + + @ValidateBoolean({ optional: true }) + withPartners?: boolean; +} + +export class MemoryLaneDto { + @IsInt() + @Type(() => Number) + @Max(31) + @Min(1) + @ApiProperty({ type: 'integer' }) + day!: number; + + @IsInt() + @Type(() => Number) + @Max(12) + @Min(1) + @ApiProperty({ type: 'integer' }) + month!: number; +} +export class MapMarkerResponseDto { + @ApiProperty() + id!: string; + + @ApiProperty({ format: 'double' }) + lat!: number; + + @ApiProperty({ format: 'double' }) + lon!: number; + + @ApiProperty() + city!: string | null; + + @ApiProperty() + state!: string | null; + + @ApiProperty() + country!: string | null; +} diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/dtos/server-info.dto.ts similarity index 93% rename from server/src/domain/server-info/server-info.dto.ts rename to server/src/dtos/server-info.dto.ts index 99d4f1566..497b7ab5e 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/dtos/server-info.dto.ts @@ -1,7 +1,8 @@ -import { FeatureFlags, IVersion, type VersionType } from '@app/domain'; import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; import type { DateTime } from 'luxon'; -import { SystemConfigThemeDto } from '../system-config/dto/system-config-theme.dto'; +import { FeatureFlags } from 'src/cores/system-config.core'; +import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; +import { IVersion, VersionType } from 'src/utils/version'; export class ServerPingResponse { @ApiResponseProperty({ type: String, example: 'pong' }) diff --git a/server/src/domain/shared-link/shared-link-response.dto.ts b/server/src/dtos/shared-link.dto.ts similarity index 55% rename from server/src/domain/shared-link/shared-link-response.dto.ts rename to server/src/dtos/shared-link.dto.ts index b16a578f4..9a90901d2 100644 --- a/server/src/domain/shared-link/shared-link-response.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,9 +1,81 @@ -import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; import _ from 'lodash'; -import { AlbumResponseDto, mapAlbumWithoutAssets } from '../album'; -import { AssetResponseDto, mapAsset } from '../asset'; +import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +export class SharedLinkCreateDto { + @IsEnum(SharedLinkType) + @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) + type!: SharedLinkType; + + @ValidateUUID({ each: true, optional: true }) + assetIds?: string[]; + + @ValidateUUID({ optional: true }) + albumId?: string; + + @IsString() + @Optional() + description?: string; + + @IsString() + @Optional() + password?: string; + + @ValidateDate({ optional: true, nullable: true }) + expiresAt?: Date | null = null; + + @ValidateBoolean({ optional: true }) + allowUpload?: boolean; + + @ValidateBoolean({ optional: true }) + allowDownload?: boolean = true; + + @ValidateBoolean({ optional: true }) + showMetadata?: boolean = true; +} + +export class SharedLinkEditDto { + @Optional() + description?: string; + + @Optional() + password?: string; + + @Optional({ nullable: true }) + expiresAt?: Date | null; + + @Optional() + allowUpload?: boolean; + + @ValidateBoolean({ optional: true }) + allowDownload?: boolean; + + @ValidateBoolean({ optional: true }) + showMetadata?: boolean; + + /** + * Few clients cannot send null to set the expiryTime to never. + * Setting this flag and not sending expiryAt is considered as null instead. + * Clients that can send null values can ignore this. + */ + @ValidateBoolean({ optional: true }) + changeExpiryTime?: boolean; +} + +export class SharedLinkPasswordDto { + @IsString() + @Optional() + @ApiProperty({ example: 'password' }) + password?: string; + + @IsString() + @Optional() + token?: string; +} export class SharedLinkResponseDto { id!: string; description!: string | null; diff --git a/server/src/domain/asset/dto/asset-stack.dto.ts b/server/src/dtos/stack.dto.ts similarity index 71% rename from server/src/domain/asset/dto/asset-stack.dto.ts rename to server/src/dtos/stack.dto.ts index 80dabdb34..3ff04ee5e 100644 --- a/server/src/domain/asset/dto/asset-stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,4 +1,4 @@ -import { ValidateUUID } from '../../domain.util'; +import { ValidateUUID } from 'src/validation'; export class UpdateStackParentDto { @ValidateUUID() diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts new file mode 100644 index 000000000..9f80e8d6a --- /dev/null +++ b/server/src/dtos/system-config.dto.ts @@ -0,0 +1,525 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsInt, + IsNotEmpty, + IsNumber, + IsObject, + IsPositive, + IsString, + IsUrl, + Max, + Min, + Validate, + ValidateIf, + ValidateNested, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; +import { + AudioCodec, + CQMode, + Colorspace, + ImageFormat, + LogLevel, + SystemConfig, + ToneMapping, + TranscodeHWAccel, + TranscodePolicy, + VideoCodec, +} from 'src/entities/system-config.entity'; +import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; +import { ValidateBoolean, validateCronExpression } from 'src/validation'; + +@ValidatorConstraint({ name: 'cronValidator' }) +class CronValidator implements ValidatorConstraintInterface { + validate(expression: string): boolean { + return validateCronExpression(expression); + } +} + +const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; +const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; +const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; + +export class SystemConfigFFmpegDto { + @IsInt() + @Min(0) + @Max(51) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + crf!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + threads!: number; + + @IsString() + preset!: string; + + @IsEnum(VideoCodec) + @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) + targetVideoCodec!: VideoCodec; + + @IsEnum(VideoCodec, { each: true }) + @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) + acceptedVideoCodecs!: VideoCodec[]; + + @IsEnum(AudioCodec) + @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) + targetAudioCodec!: AudioCodec; + + @IsEnum(AudioCodec, { each: true }) + @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) + acceptedAudioCodecs!: AudioCodec[]; + + @IsString() + targetResolution!: string; + + @IsString() + maxBitrate!: string; + + @IsInt() + @Min(-1) + @Max(16) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + bframes!: number; + + @IsInt() + @Min(0) + @Max(6) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + refs!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + gopSize!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + npl!: number; + + @ValidateBoolean() + temporalAQ!: boolean; + + @IsEnum(CQMode) + @ApiProperty({ enumName: 'CQMode', enum: CQMode }) + cqMode!: CQMode; + + @ValidateBoolean() + twoPass!: boolean; + + @IsString() + preferredHwDevice!: string; + + @IsEnum(TranscodePolicy) + @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) + transcode!: TranscodePolicy; + + @IsEnum(TranscodeHWAccel) + @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) + accel!: TranscodeHWAccel; + + @IsEnum(ToneMapping) + @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) + tonemap!: ToneMapping; +} + +class JobSettingsDto { + @IsInt() + @IsPositive() + @ApiProperty({ type: 'integer' }) + concurrency!: number; +} + +class SystemConfigJobDto implements Record { + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SMART_SEARCH]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.MIGRATION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.BACKGROUND_TASK]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SEARCH]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.FACE_DETECTION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SIDECAR]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.LIBRARY]!: JobSettingsDto; +} + +class SystemConfigLibraryScanDto { + @ValidateBoolean() + enabled!: boolean; + + @ValidateIf(isLibraryScanEnabled) + @IsNotEmpty() + @Validate(CronValidator, { message: 'Invalid cron expression' }) + @IsString() + cronExpression!: string; +} + +class SystemConfigLibraryWatchDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigLibraryDto { + @Type(() => SystemConfigLibraryScanDto) + @ValidateNested() + @IsObject() + scan!: SystemConfigLibraryScanDto; + + @Type(() => SystemConfigLibraryWatchDto) + @ValidateNested() + @IsObject() + watch!: SystemConfigLibraryWatchDto; +} + +class SystemConfigLoggingDto { + @ValidateBoolean() + enabled!: boolean; + + @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) + @IsEnum(LogLevel) + level!: LogLevel; +} + +class SystemConfigMachineLearningDto { + @ValidateBoolean() + enabled!: boolean; + + @IsUrl({ require_tld: false, allow_underscores: true }) + @ValidateIf((dto) => dto.enabled) + url!: string; + + @Type(() => CLIPConfig) + @ValidateNested() + @IsObject() + clip!: CLIPConfig; + + @Type(() => RecognitionConfig) + @ValidateNested() + @IsObject() + facialRecognition!: RecognitionConfig; +} + +enum MapTheme { + LIGHT = 'light', + DARK = 'dark', +} + +export class MapThemeDto { + @IsEnum(MapTheme) + @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) + theme!: MapTheme; +} + +class SystemConfigMapDto { + @ValidateBoolean() + enabled!: boolean; + + @IsString() + lightStyle!: string; + + @IsString() + darkStyle!: string; +} + +class SystemConfigNewVersionCheckDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigOAuthDto { + @ValidateBoolean() + autoLaunch!: boolean; + + @ValidateBoolean() + autoRegister!: boolean; + + @IsString() + buttonText!: string; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + clientId!: string; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + clientSecret!: string; + + @IsNumber() + @Min(0) + defaultStorageQuota!: number; + + @ValidateBoolean() + enabled!: boolean; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + issuerUrl!: string; + + @ValidateBoolean() + mobileOverrideEnabled!: boolean; + + @ValidateIf(isOAuthOverrideEnabled) + @IsUrl() + mobileRedirectUri!: string; + + @IsString() + scope!: string; + + @IsString() + @IsNotEmpty() + signingAlgorithm!: string; + + @IsString() + storageLabelClaim!: string; + + @IsString() + storageQuotaClaim!: string; +} + +class SystemConfigPasswordLoginDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigReverseGeocodingDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigServerDto { + @IsString() + externalDomain!: string; + + @IsString() + loginPageMessage!: string; +} + +class SystemConfigStorageTemplateDto { + @ValidateBoolean() + enabled!: boolean; + + @ValidateBoolean() + hashVerificationEnabled!: boolean; + + @IsNotEmpty() + @IsString() + template!: string; +} + +export class SystemConfigTemplateStorageOptionDto { + yearOptions!: string[]; + monthOptions!: string[]; + weekOptions!: string[]; + dayOptions!: string[]; + hourOptions!: string[]; + minuteOptions!: string[]; + secondOptions!: string[]; + presetOptions!: string[]; +} + +export class SystemConfigThemeDto { + @IsString() + customCss!: string; +} + +class SystemConfigImageDto { + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + thumbnailFormat!: ImageFormat; + + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + thumbnailSize!: number; + + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + previewFormat!: ImageFormat; + + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + previewSize!: number; + + @IsInt() + @Min(1) + @Max(100) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + quality!: number; + + @IsEnum(Colorspace) + @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) + colorspace!: Colorspace; +} + +class SystemConfigTrashDto { + @ValidateBoolean() + enabled!: boolean; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + days!: number; +} + +class SystemConfigUserDto { + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + deleteDelay!: number; +} + +export class SystemConfigDto implements SystemConfig { + @Type(() => SystemConfigFFmpegDto) + @ValidateNested() + @IsObject() + ffmpeg!: SystemConfigFFmpegDto; + + @Type(() => SystemConfigLoggingDto) + @ValidateNested() + @IsObject() + logging!: SystemConfigLoggingDto; + + @Type(() => SystemConfigMachineLearningDto) + @ValidateNested() + @IsObject() + machineLearning!: SystemConfigMachineLearningDto; + + @Type(() => SystemConfigMapDto) + @ValidateNested() + @IsObject() + map!: SystemConfigMapDto; + + @Type(() => SystemConfigNewVersionCheckDto) + @ValidateNested() + @IsObject() + newVersionCheck!: SystemConfigNewVersionCheckDto; + + @Type(() => SystemConfigOAuthDto) + @ValidateNested() + @IsObject() + oauth!: SystemConfigOAuthDto; + + @Type(() => SystemConfigPasswordLoginDto) + @ValidateNested() + @IsObject() + passwordLogin!: SystemConfigPasswordLoginDto; + + @Type(() => SystemConfigReverseGeocodingDto) + @ValidateNested() + @IsObject() + reverseGeocoding!: SystemConfigReverseGeocodingDto; + + @Type(() => SystemConfigStorageTemplateDto) + @ValidateNested() + @IsObject() + storageTemplate!: SystemConfigStorageTemplateDto; + + @Type(() => SystemConfigJobDto) + @ValidateNested() + @IsObject() + job!: SystemConfigJobDto; + + @Type(() => SystemConfigImageDto) + @ValidateNested() + @IsObject() + image!: SystemConfigImageDto; + + @Type(() => SystemConfigTrashDto) + @ValidateNested() + @IsObject() + trash!: SystemConfigTrashDto; + + @Type(() => SystemConfigThemeDto) + @ValidateNested() + @IsObject() + theme!: SystemConfigThemeDto; + + @Type(() => SystemConfigLibraryDto) + @ValidateNested() + @IsObject() + library!: SystemConfigLibraryDto; + + @Type(() => SystemConfigServerDto) + @ValidateNested() + @IsObject() + server!: SystemConfigServerDto; + + @Type(() => SystemConfigUserDto) + @ValidateNested() + @IsObject() + user!: SystemConfigUserDto; +} + +export function mapConfig(config: SystemConfig): SystemConfigDto { + return config; +} diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts new file mode 100644 index 000000000..1094d70df --- /dev/null +++ b/server/src/dtos/tag.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { TagEntity, TagType } from 'src/entities/tag.entity'; +import { Optional } from 'src/validation'; + +export class CreateTagDto { + @IsString() + @IsNotEmpty() + name!: string; + + @IsEnum(TagType) + @IsNotEmpty() + @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) + type!: TagType; +} + +export class UpdateTagDto { + @IsString() + @Optional() + name?: string; +} + +export class TagResponseDto { + id!: string; + @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) + type!: string; + name!: string; + userId!: string; +} + +export function mapTag(entity: TagEntity): TagResponseDto { + return { + id: entity.id, + type: entity.type, + name: entity.name, + userId: entity.userId, + }; +} diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts similarity index 73% rename from server/src/domain/asset/dto/time-bucket.dto.ts rename to server/src/dtos/time-bucket.dto.ts index 7c5b9c212..a55126013 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,8 +1,8 @@ -import { AssetOrder } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; -import { TimeBucketSize } from '../../repositories'; +import { AssetOrder } from 'src/entities/album.entity'; +import { TimeBucketSize } from 'src/interfaces/asset.interface'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class TimeBucketDto { @IsNotEmpty() @@ -44,3 +44,11 @@ export class TimeBucketAssetDto extends TimeBucketDto { @IsString() timeBucket!: string; } + +export class TimeBucketResponseDto { + @ApiProperty({ type: 'string' }) + timeBucket!: string; + + @ApiProperty({ type: 'integer' }) + count!: number; +} diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts new file mode 100644 index 000000000..2f3d8cf22 --- /dev/null +++ b/server/src/dtos/user-profile.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UploadFieldName } from 'src/dtos/asset.dto'; +import { UserAvatarColor, UserEntity } from 'src/entities/user.entity'; + +export class CreateProfileImageDto { + @ApiProperty({ type: 'string', format: 'binary' }) + [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; +} + +export class CreateProfileImageResponseDto { + userId!: string; + profileImagePath!: string; +} + +export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { + return { + userId: userId, + profileImagePath: profileImagePath, + }; +} + +export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, + ); + return values[randomIndex] as UserAvatarColor; +}; diff --git a/server/src/domain/user/dto/create-user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts similarity index 79% rename from server/src/domain/user/dto/create-user.dto.spec.ts rename to server/src/dtos/user.dto.spec.ts index 4e571d38a..d07399f0e 100644 --- a/server/src/domain/user/dto/create-user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,6 +1,20 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; -import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './create-user.dto'; +import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto'; + +describe('update user DTO', () => { + it('should allow emails without a tld', async () => { + const someEmail = 'test@test'; + + const dto = plainToInstance(UpdateUserDto, { + email: someEmail, + id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', + }); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + expect(dto.email).toEqual(someEmail); + }); +}); describe('create user DTO', () => { it('validates the email', async () => { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts new file mode 100644 index 000000000..309006822 --- /dev/null +++ b/server/src/dtos/user.dto.ts @@ -0,0 +1,169 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; +import { getRandomAvatarColor } from 'src/dtos/user-profile.dto'; +import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity'; +import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; + +export class CreateUserDto { + @IsEmail({ require_tld: false }) + @Transform(toEmail) + email!: string; + + @IsNotEmpty() + @IsString() + password!: string; + + @IsNotEmpty() + @IsString() + name!: string; + + @Optional({ nullable: true }) + @IsString() + @Transform(toSanitized) + storageLabel?: string | null; + + @ValidateBoolean({ optional: true }) + memoriesEnabled?: boolean; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; + + @ValidateBoolean({ optional: true }) + shouldChangePassword?: boolean; +} + +export class CreateAdminDto { + @IsNotEmpty() + isAdmin!: true; + + @IsEmail({ require_tld: false }) + @Transform(({ value }) => value?.toLowerCase()) + email!: string; + + @IsNotEmpty() + password!: string; + + @IsNotEmpty() + name!: string; +} + +export class CreateUserOAuthDto { + @IsEmail({ require_tld: false }) + @Transform(({ value }) => value?.toLowerCase()) + email!: string; + + @IsNotEmpty() + oauthId!: string; + + name?: string; +} + +export class DeleteUserDto { + @ValidateBoolean({ optional: true }) + force?: boolean; +} + +export class UpdateUserDto { + @Optional() + @IsEmail({ require_tld: false }) + @Transform(toEmail) + email?: string; + + @Optional() + @IsNotEmpty() + @IsString() + password?: string; + + @Optional() + @IsString() + @IsNotEmpty() + name?: string; + + @Optional() + @IsString() + @Transform(toSanitized) + storageLabel?: string; + + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + id!: string; + + @ValidateBoolean({ optional: true }) + isAdmin?: boolean; + + @ValidateBoolean({ optional: true }) + shouldChangePassword?: boolean; + + @ValidateBoolean({ optional: true }) + memoriesEnabled?: boolean; + + @Optional() + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; +} + +export class UserDto { + id!: string; + name!: string; + email!: string; + profileImagePath!: string; + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor!: UserAvatarColor; +} + +export class UserResponseDto extends UserDto { + storageLabel!: string | null; + shouldChangePassword!: boolean; + isAdmin!: boolean; + createdAt!: Date; + deletedAt!: Date | null; + updatedAt!: Date; + oauthId!: string; + memoriesEnabled?: boolean; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes!: number | null; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaUsageInBytes!: number | null; + @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) + status!: string; +} + +export const mapSimpleUser = (entity: UserEntity): UserDto => { + return { + id: entity.id, + email: entity.email, + name: entity.name, + profileImagePath: entity.profileImagePath, + avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), + }; +}; + +export function mapUser(entity: UserEntity): UserResponseDto { + return { + ...mapSimpleUser(entity), + storageLabel: entity.storageLabel, + shouldChangePassword: entity.shouldChangePassword, + isAdmin: entity.isAdmin, + createdAt: entity.createdAt, + deletedAt: entity.deletedAt, + updatedAt: entity.updatedAt, + oauthId: entity.oauthId, + memoriesEnabled: entity.memoriesEnabled, + quotaSizeInBytes: entity.quotaSizeInBytes, + quotaUsageInBytes: entity.quotaUsageInBytes, + status: entity.status, + }; +} diff --git a/server/src/infra/entities/activity.entity.ts b/server/src/entities/activity.entity.ts similarity index 87% rename from server/src/infra/entities/activity.entity.ts rename to server/src/entities/activity.entity.ts index 255a3a708..8de76ac89 100644 --- a/server/src/infra/entities/activity.entity.ts +++ b/server/src/entities/activity.entity.ts @@ -1,3 +1,6 @@ +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Check, Column, @@ -8,9 +11,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AlbumEntity } from './album.entity'; -import { AssetEntity } from './asset.entity'; -import { UserEntity } from './user.entity'; @Entity('activity') @Index('IDX_activity_like', ['assetId', 'userId', 'albumId'], { unique: true, where: '("isLiked" = true)' }) diff --git a/server/src/infra/entities/album.entity.ts b/server/src/entities/album.entity.ts similarity index 89% rename from server/src/infra/entities/album.entity.ts rename to server/src/entities/album.entity.ts index daa8fcbc3..99fae4f23 100644 --- a/server/src/infra/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -1,3 +1,6 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, @@ -10,9 +13,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AssetEntity } from './asset.entity'; -import { SharedLinkEntity } from './shared-link.entity'; -import { UserEntity } from './user.entity'; // ran into issues when importing the enum from `asset.dto.ts` export enum AssetOrder { diff --git a/server/src/infra/entities/api-key.entity.ts b/server/src/entities/api-key.entity.ts similarity index 90% rename from server/src/infra/entities/api-key.entity.ts rename to server/src/entities/api-key.entity.ts index 0c8252fe4..18aaa8304 100644 --- a/server/src/infra/entities/api-key.entity.ts +++ b/server/src/entities/api-key.entity.ts @@ -1,5 +1,5 @@ +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -import { UserEntity } from './user.entity'; @Entity('api_keys') export class APIKeyEntity { diff --git a/server/src/infra/entities/asset-face.entity.ts b/server/src/entities/asset-face.entity.ts similarity index 91% rename from server/src/infra/entities/asset-face.entity.ts rename to server/src/entities/asset-face.entity.ts index 1561f67d0..38fcd4606 100644 --- a/server/src/infra/entities/asset-face.entity.ts +++ b/server/src/entities/asset-face.entity.ts @@ -1,6 +1,6 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { PersonEntity } from 'src/entities/person.entity'; import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { AssetEntity } from './asset.entity'; -import { PersonEntity } from './person.entity'; @Entity('asset_faces', { synchronize: false }) @Index('IDX_asset_faces_assetId_personId', ['assetId', 'personId']) diff --git a/server/src/infra/entities/asset-job-status.entity.ts b/server/src/entities/asset-job-status.entity.ts similarity index 89% rename from server/src/infra/entities/asset-job-status.entity.ts rename to server/src/entities/asset-job-status.entity.ts index f1965fbdb..b50075203 100644 --- a/server/src/infra/entities/asset-job-status.entity.ts +++ b/server/src/entities/asset-job-status.entity.ts @@ -1,5 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -import { AssetEntity } from './asset.entity'; @Entity('asset_job_status') export class AssetJobStatusEntity { diff --git a/server/src/infra/entities/asset-stack.entity.ts b/server/src/entities/asset-stack.entity.ts similarity index 89% rename from server/src/infra/entities/asset-stack.entity.ts rename to server/src/entities/asset-stack.entity.ts index d005fc0a5..28607e7ab 100644 --- a/server/src/infra/entities/asset-stack.entity.ts +++ b/server/src/entities/asset-stack.entity.ts @@ -1,5 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { AssetEntity } from './asset.entity'; @Entity('asset_stack') export class AssetStackEntity { diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/entities/asset.entity.ts similarity index 85% rename from server/src/infra/entities/asset.entity.ts rename to server/src/entities/asset.entity.ts index 78a961757..c977560be 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,3 +1,14 @@ +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; +import { AssetStackEntity } from 'src/entities/asset-stack.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { LibraryEntity } from 'src/entities/library.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { SmartSearchEntity } from 'src/entities/smart-search.entity'; +import { TagEntity } from 'src/entities/tag.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, @@ -13,17 +24,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AlbumEntity } from './album.entity'; -import { AssetFaceEntity } from './asset-face.entity'; -import { AssetJobStatusEntity } from './asset-job-status.entity'; -import { AssetStackEntity } from './asset-stack.entity'; -import { ExifEntity } from './exif.entity'; -import { LibraryEntity } from './library.entity'; -import { SharedLinkEntity } from './shared-link.entity'; -import { SmartInfoEntity } from './smart-info.entity'; -import { SmartSearchEntity } from './smart-search.entity'; -import { TagEntity } from './tag.entity'; -import { UserEntity } from './user.entity'; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum'; @@ -67,10 +67,10 @@ export class AssetEntity { originalPath!: string; @Column({ type: 'varchar', nullable: true }) - resizePath!: string | null; + previewPath!: string | null; @Column({ type: 'varchar', nullable: true, default: '' }) - webpPath!: string | null; + thumbnailPath!: string | null; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/infra/entities/audit.entity.ts b/server/src/entities/audit.entity.ts similarity index 100% rename from server/src/infra/entities/audit.entity.ts rename to server/src/entities/audit.entity.ts diff --git a/server/src/infra/entities/exif.entity.ts b/server/src/entities/exif.entity.ts similarity index 98% rename from server/src/infra/entities/exif.entity.ts rename to server/src/entities/exif.entity.ts index 639e24a50..6f7aafadf 100644 --- a/server/src/infra/entities/exif.entity.ts +++ b/server/src/entities/exif.entity.ts @@ -1,7 +1,7 @@ +import { AssetEntity } from 'src/entities/asset.entity'; import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; import { Column } from 'typeorm/decorator/columns/Column.js'; import { Entity } from 'typeorm/decorator/entity/Entity.js'; -import { AssetEntity } from './asset.entity'; @Entity('exif') export class ExifEntity { diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/entities/geodata-places.entity.ts similarity index 100% rename from server/src/infra/entities/geodata-places.entity.ts rename to server/src/entities/geodata-places.entity.ts diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts new file mode 100644 index 000000000..761b47693 --- /dev/null +++ b/server/src/entities/index.ts @@ -0,0 +1,49 @@ +import { ActivityEntity } from 'src/entities/activity.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; +import { AssetStackEntity } from 'src/entities/asset-stack.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { LibraryEntity } from 'src/entities/library.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { MoveEntity } from 'src/entities/move.entity'; +import { PartnerEntity } from 'src/entities/partner.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { SmartSearchEntity } from 'src/entities/smart-search.entity'; +import { SystemConfigEntity } from 'src/entities/system-config.entity'; +import { SystemMetadataEntity } from 'src/entities/system-metadata.entity'; +import { TagEntity } from 'src/entities/tag.entity'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { UserEntity } from 'src/entities/user.entity'; + +export const entities = [ + ActivityEntity, + AlbumEntity, + APIKeyEntity, + AssetEntity, + AssetStackEntity, + AssetFaceEntity, + AssetJobStatusEntity, + AuditEntity, + ExifEntity, + GeodataPlacesEntity, + MemoryEntity, + MoveEntity, + PartnerEntity, + PersonEntity, + SharedLinkEntity, + SmartInfoEntity, + SmartSearchEntity, + SystemConfigEntity, + SystemMetadataEntity, + TagEntity, + UserEntity, + UserTokenEntity, + LibraryEntity, +]; diff --git a/server/src/infra/entities/library.entity.ts b/server/src/entities/library.entity.ts similarity index 91% rename from server/src/infra/entities/library.entity.ts rename to server/src/entities/library.entity.ts index bf5f444ab..8be560a88 100644 --- a/server/src/infra/entities/library.entity.ts +++ b/server/src/entities/library.entity.ts @@ -1,3 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, @@ -9,8 +11,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AssetEntity } from './asset.entity'; -import { UserEntity } from './user.entity'; @Entity('libraries') export class LibraryEntity { diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts new file mode 100644 index 000000000..d7dcff4b8 --- /dev/null +++ b/server/src/entities/memory.entity.ts @@ -0,0 +1,67 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +export enum MemoryType { + /** pictures taken on this day X years ago */ + ON_THIS_DAY = 'on_this_day', +} + +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; + + @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; + + /** when the user last viewed the memory */ + @Column({ type: 'timestamptz', nullable: true }) + seenAt?: Date; + + @ManyToMany(() => AssetEntity) + @JoinTable() + assets!: AssetEntity[]; +} diff --git a/server/src/infra/entities/move.entity.ts b/server/src/entities/move.entity.ts similarity index 91% rename from server/src/infra/entities/move.entity.ts rename to server/src/entities/move.entity.ts index de20cb973..f3dad6b28 100644 --- a/server/src/infra/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -24,8 +24,8 @@ export class MoveEntity { export enum AssetPathType { ORIGINAL = 'original', - JPEG_THUMBNAIL = 'jpeg_thumbnail', - WEBP_THUMBNAIL = 'webp_thumbnail', + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded_video', SIDECAR = 'sidecar', } diff --git a/server/src/infra/entities/partner.entity.ts b/server/src/entities/partner.entity.ts similarity index 93% rename from server/src/infra/entities/partner.entity.ts rename to server/src/entities/partner.entity.ts index 35d32e4c9..189f6f51a 100644 --- a/server/src/infra/entities/partner.entity.ts +++ b/server/src/entities/partner.entity.ts @@ -1,7 +1,6 @@ +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; -import { UserEntity } from './user.entity'; - @Entity('partners') export class PartnerEntity { @PrimaryColumn('uuid') diff --git a/server/src/infra/entities/person.entity.ts b/server/src/entities/person.entity.ts similarity index 89% rename from server/src/infra/entities/person.entity.ts rename to server/src/entities/person.entity.ts index ecba45dd2..bc60efcd6 100644 --- a/server/src/infra/entities/person.entity.ts +++ b/server/src/entities/person.entity.ts @@ -1,3 +1,5 @@ +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Check, Column, @@ -8,8 +10,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AssetFaceEntity } from './asset-face.entity'; -import { UserEntity } from './user.entity'; @Entity('person') @Check(`"birthDate" <= CURRENT_DATE`) diff --git a/server/src/infra/entities/shared-link.entity.ts b/server/src/entities/shared-link.entity.ts similarity index 90% rename from server/src/infra/entities/shared-link.entity.ts rename to server/src/entities/shared-link.entity.ts index e7cd19e53..f328192f7 100644 --- a/server/src/infra/entities/shared-link.entity.ts +++ b/server/src/entities/shared-link.entity.ts @@ -1,3 +1,6 @@ +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, @@ -8,9 +11,6 @@ import { PrimaryGeneratedColumn, Unique, } from 'typeorm'; -import { AlbumEntity } from './album.entity'; -import { AssetEntity } from './asset.entity'; -import { UserEntity } from './user.entity'; @Entity('shared_links') @Unique('UQ_sharedlink_key', ['key']) diff --git a/server/src/infra/entities/smart-info.entity.ts b/server/src/entities/smart-info.entity.ts similarity index 90% rename from server/src/infra/entities/smart-info.entity.ts rename to server/src/entities/smart-info.entity.ts index 2606de60e..86190c174 100644 --- a/server/src/infra/entities/smart-info.entity.ts +++ b/server/src/entities/smart-info.entity.ts @@ -1,5 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -import { AssetEntity } from './asset.entity'; @Entity('smart_info', { synchronize: false }) export class SmartInfoEntity { diff --git a/server/src/infra/entities/smart-search.entity.ts b/server/src/entities/smart-search.entity.ts similarity index 90% rename from server/src/infra/entities/smart-search.entity.ts rename to server/src/entities/smart-search.entity.ts index 2b295ac90..4595ad240 100644 --- a/server/src/infra/entities/smart-search.entity.ts +++ b/server/src/entities/smart-search.entity.ts @@ -1,5 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -import { AssetEntity } from './asset.entity'; @Entity('smart_search', { synchronize: false }) export class SmartSearchEntity { diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts similarity index 96% rename from server/src/infra/entities/system-config.entity.ts rename to server/src/entities/system-config.entity.ts index 1ba219429..e07b6d4a2 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -1,4 +1,4 @@ -import { ConcurrentQueueName } from '@app/domain'; +import { ConcurrentQueueName } from 'src/interfaces/job.interface'; import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_config') @@ -165,6 +165,11 @@ export enum Colorspace { P3 = 'p3', } +export enum ImageFormat { + JPEG = 'jpeg', + WEBP = 'webp', +} + export enum LogLevel { VERBOSE = 'verbose', DEBUG = 'debug', @@ -249,9 +254,11 @@ export interface SystemConfig { hashVerificationEnabled: boolean; template: string; }; - thumbnail: { - webpSize: number; - jpegSize: number; + image: { + thumbnailFormat: ImageFormat; + thumbnailSize: number; + previewFormat: ImageFormat; + previewSize: number; quality: number; colorspace: Colorspace; }; diff --git a/server/src/infra/entities/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts similarity index 100% rename from server/src/infra/entities/system-metadata.entity.ts rename to server/src/entities/system-metadata.entity.ts diff --git a/server/src/infra/entities/tag.entity.ts b/server/src/entities/tag.entity.ts similarity index 89% rename from server/src/infra/entities/tag.entity.ts rename to server/src/entities/tag.entity.ts index a364529db..93edcb055 100644 --- a/server/src/infra/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -1,6 +1,6 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; -import { AssetEntity } from './asset.entity'; -import { UserEntity } from './user.entity'; @Entity('tags') @Unique('UQ_tag_name_userId', ['name', 'userId']) diff --git a/server/src/infra/entities/user-token.entity.ts b/server/src/entities/user-token.entity.ts similarity index 91% rename from server/src/infra/entities/user-token.entity.ts rename to server/src/entities/user-token.entity.ts index a39e93a33..3c2cf2cf6 100644 --- a/server/src/infra/entities/user-token.entity.ts +++ b/server/src/entities/user-token.entity.ts @@ -1,5 +1,5 @@ +import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -import { UserEntity } from './user.entity'; @Entity('user_token') export class UserTokenEntity { diff --git a/server/src/infra/entities/user.entity.ts b/server/src/entities/user.entity.ts similarity index 94% rename from server/src/infra/entities/user.entity.ts rename to server/src/entities/user.entity.ts index 20c057d79..4d6361aba 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -1,3 +1,5 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { TagEntity } from 'src/entities/tag.entity'; import { Column, CreateDateColumn, @@ -7,8 +9,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AssetEntity } from './asset.entity'; -import { TagEntity } from './tag.entity'; export enum UserAvatarColor { PRIMARY = 'primary', diff --git a/server/src/immich-admin/app.module.ts b/server/src/immich-admin/app.module.ts deleted file mode 100644 index b350aec83..000000000 --- a/server/src/immich-admin/app.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DomainModule } from '@app/domain'; -import { InfraModule } from '@app/infra'; -import { Module } from '@nestjs/common'; -import { ListUsersCommand } from './commands/list-users.command'; -import { DisableOAuthLogin, EnableOAuthLogin } from './commands/oauth-login'; -import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from './commands/password-login'; -import { PromptPasswordQuestions, ResetAdminPasswordCommand } from './commands/reset-admin-password.command'; - -@Module({ - imports: [InfraModule, DomainModule], - providers: [ - ResetAdminPasswordCommand, - PromptPasswordQuestions, - EnablePasswordLoginCommand, - DisablePasswordLoginCommand, - EnableOAuthLogin, - DisableOAuthLogin, - ListUsersCommand, - ], -}) -export class AppModule {} diff --git a/server/src/immich-admin/constants.ts b/server/src/immich-admin/constants.ts deleted file mode 100644 index 44fbfdf77..000000000 --- a/server/src/immich-admin/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AuthDto } from '@app/domain'; -import { UserEntity } from '@app/infra/entities'; - -export const CLI_USER: AuthDto = { - user: { - id: 'cli', - email: 'cli@immich.app', - isAdmin: true, - } as UserEntity, -}; diff --git a/server/src/immich-admin/main.ts b/server/src/immich-admin/main.ts deleted file mode 100755 index f14aac20f..000000000 --- a/server/src/immich-admin/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LogLevel } from '@app/infra/entities'; -import { CommandFactory } from 'nest-commander'; -import { AppModule } from './app.module'; - -export async function bootstrap() { - process.env.LOG_LEVEL = LogLevel.WARN; - await CommandFactory.run(AppModule); -} diff --git a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts b/server/src/immich/api-v1/asset/dto/asset-check.dto.ts deleted file mode 100644 index d3474171f..000000000 --- a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; - -export class AssetBulkUploadCheckItem { - @IsString() - @IsNotEmpty() - id!: string; - - /** base64 or hex encoded sha1 hash */ - @IsString() - @IsNotEmpty() - checksum!: string; -} - -export class AssetBulkUploadCheckDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetBulkUploadCheckItem) - assets!: AssetBulkUploadCheckItem[]; -} diff --git a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts deleted file mode 100644 index 719018488..000000000 --- a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Optional, ValidateBoolean, ValidateDate } from '@app/domain'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsUUID } from 'class-validator'; - -export class AssetSearchDto { - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @Optional() - @IsInt() - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - skip?: number; - - @Optional() - @IsInt() - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - take?: number; - - @Optional() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - userId?: string; - - @ValidateDate({ optional: true }) - updatedAfter?: Date; - - @ValidateDate({ optional: true }) - updatedBefore?: Date; -} diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts deleted file mode 100644 index dd0eca03f..000000000 --- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; -import { CheckExistingAssetsDto } from './check-existing-assets.dto'; - -describe('CheckExistingAssetsDto', () => { - it('should fail with an empty list', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [], deviceId: 'test-device' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('deviceAssetIds'); - }); - - it('should fail with an empty string', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [''], deviceId: 'test-device' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('deviceAssetIds'); - }); - - it('should work with valid asset ids', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { - deviceAssetIds: ['asset-1', 'asset-2'], - deviceId: 'test-device', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - }); -}); diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts deleted file mode 100644 index 65740ab89..000000000 --- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArrayNotEmpty, IsNotEmpty, IsString } from 'class-validator'; - -export class CheckExistingAssetsDto { - @ArrayNotEmpty() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - deviceAssetIds!: string[]; - - @IsNotEmpty() - deviceId!: string; -} diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts deleted file mode 100644 index 1b140d69f..000000000 --- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Optional, UploadFieldName, ValidateBoolean, ValidateDate, ValidateUUID } from '@app/domain'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class CreateAssetDto { - @ValidateUUID({ optional: true }) - libraryId?: string; - - @IsNotEmpty() - @IsString() - deviceAssetId!: string; - - @IsNotEmpty() - @IsString() - deviceId!: string; - - @ValidateDate() - fileCreatedAt!: Date; - - @ValidateDate() - fileModifiedAt!: Date; - - @Optional() - @IsString() - duration?: string; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isVisible?: boolean; - - @ValidateBoolean({ optional: true }) - isOffline?: boolean; - - @ValidateBoolean({ optional: true }) - isReadOnly?: boolean; - - // The properties below are added to correctly generate the API docs - // and client SDKs. Validation should be handled in the controller. - @ApiProperty({ type: 'string', format: 'binary' }) - [UploadFieldName.ASSET_DATA]!: any; - - @ApiProperty({ type: 'string', format: 'binary', required: false }) - [UploadFieldName.LIVE_PHOTO_DATA]?: any; - - @ApiProperty({ type: 'string', format: 'binary', required: false }) - [UploadFieldName.SIDECAR_DATA]?: any; -} diff --git a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts deleted file mode 100644 index da5661e0d..000000000 --- a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Optional } from '@app/domain'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; - -export enum GetAssetThumbnailFormatEnum { - JPEG = 'JPEG', - WEBP = 'WEBP', -} - -export class GetAssetThumbnailDto { - @Optional() - @IsEnum(GetAssetThumbnailFormatEnum) - @ApiProperty({ - type: String, - enum: GetAssetThumbnailFormatEnum, - default: GetAssetThumbnailFormatEnum.WEBP, - required: false, - enumName: 'ThumbnailFormat', - }) - format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP; -} diff --git a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts b/server/src/immich/api-v1/asset/dto/search-properties.dto.ts deleted file mode 100644 index 669b29b2e..000000000 --- a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class SearchPropertiesDto { - tags?: string[]; - objects?: string[]; - assetType?: string; - orientation?: string; - lensModel?: string; - make?: string; - model?: string; - city?: string; - state?: string; - country?: string; -} diff --git a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts b/server/src/immich/api-v1/asset/dto/serve-file.dto.ts deleted file mode 100644 index 72e228601..000000000 --- a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ValidateBoolean } from '@app/domain'; -import { ApiProperty } from '@nestjs/swagger'; - -export class ServeFileDto { - @ValidateBoolean({ optional: true }) - @ApiProperty({ title: 'Is serve thumbnail (resize) file' }) - isThumb?: boolean; - - @ValidateBoolean({ optional: true }) - @ApiProperty({ title: 'Is request made from web' }) - isWeb?: boolean; -} diff --git a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts deleted file mode 100644 index 1a51dc53f..000000000 --- a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class AssetBulkUploadCheckResult { - id!: string; - action!: AssetUploadAction; - reason?: AssetRejectReason; - assetId?: string; -} - -export class AssetBulkUploadCheckResponseDto { - results!: AssetBulkUploadCheckResult[]; -} - -export enum AssetUploadAction { - ACCEPT = 'accept', - REJECT = 'reject', -} - -export enum AssetRejectReason { - DUPLICATE = 'duplicate', - UNSUPPORTED_FORMAT = 'unsupported-format', -} diff --git a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts deleted file mode 100644 index f628b708d..000000000 --- a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class AssetFileUploadResponseDto { - id!: string; - duplicate!: boolean; -} diff --git a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts deleted file mode 100644 index c39a79606..000000000 --- a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class CheckExistingAssetsResponseDto { - existingIds!: string[]; -} diff --git a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts deleted file mode 100644 index 63b1b0969..000000000 --- a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class CuratedLocationsResponseDto { - id!: string; - city!: string; - resizePath!: string; - deviceAssetId!: string; - deviceId!: string; -} diff --git a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts deleted file mode 100644 index 0d23b3eb7..000000000 --- a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class CuratedObjectsResponseDto { - id!: string; - object!: string; - resizePath!: string; - deviceAssetId!: string; - deviceId!: string; -} diff --git a/server/src/immich/api-v1/validation/file-not-empty-validator.ts b/server/src/immich/api-v1/validation/file-not-empty-validator.ts deleted file mode 100644 index 21f93a952..000000000 --- a/server/src/immich/api-v1/validation/file-not-empty-validator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { FileValidator, Injectable } from '@nestjs/common'; - -@Injectable() -export default class FileNotEmptyValidator extends FileValidator { - constructor(private requiredFields: string[]) { - super({}); - this.requiredFields = requiredFields; - } - - isValid(files?: any): boolean { - if (!files) { - return false; - } - - return this.requiredFields.every((field) => files[field]); - } - - buildErrorMessage(): string { - return `Field(s) ${this.requiredFields.join(', ')} should not be empty`; - } -} diff --git a/server/src/immich/api-v1/validation/parse-me-uuid-pipe.ts b/server/src/immich/api-v1/validation/parse-me-uuid-pipe.ts deleted file mode 100644 index 4329af011..000000000 --- a/server/src/immich/api-v1/validation/parse-me-uuid-pipe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArgumentMetadata, Injectable, ParseUUIDPipe } from '@nestjs/common'; - -@Injectable() -export class ParseMeUUIDPipe extends ParseUUIDPipe { - async transform(value: string, metadata: ArgumentMetadata) { - if (value == 'me') { - return value; - } - return super.transform(value, metadata); - } -} diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts deleted file mode 100644 index 662f45f7c..000000000 --- a/server/src/immich/app.module.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { DomainModule } from '@app/domain'; -import { InfraModule } from '@app/infra'; -import { AssetEntity, ExifEntity } from '@app/infra/entities'; -import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common'; -import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { ScheduleModule } from '@nestjs/schedule'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AssetRepositoryV1, IAssetRepositoryV1 } from './api-v1/asset/asset-repository'; -import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller'; -import { AssetService } from './api-v1/asset/asset.service'; -import { AppGuard } from './app.guard'; -import { AppService } from './app.service'; -import { - APIKeyController, - ActivityController, - AlbumController, - AppController, - AssetController, - AssetsController, - AuditController, - AuthController, - DownloadController, - FaceController, - JobController, - LibraryController, - OAuthController, - PartnerController, - PersonController, - SearchController, - ServerInfoController, - SharedLinkController, - SystemConfigController, - TagController, - TrashController, - UserController, -} from './controllers'; -import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; - -@Module({ - imports: [ - // - InfraModule, - DomainModule, - ScheduleModule.forRoot(), - TypeOrmModule.forFeature([AssetEntity, ExifEntity]), - ], - controllers: [ - ActivityController, - AssetsController, - AssetControllerV1, - AssetController, - AppController, - AlbumController, - APIKeyController, - AuditController, - AuthController, - DownloadController, - FaceController, - JobController, - LibraryController, - OAuthController, - PartnerController, - SearchController, - ServerInfoController, - SharedLinkController, - SystemConfigController, - TagController, - TrashController, - UserController, - PersonController, - ], - providers: [ - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, - { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, - { provide: APP_GUARD, useClass: AppGuard }, - { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 }, - AppService, - AssetService, - FileUploadInterceptor, - ], -}) -export class AppModule implements OnModuleInit { - constructor(private appService: AppService) {} - - async onModuleInit() { - await this.appService.init(); - } -} diff --git a/server/src/immich/controllers/dto/uuid-param.dto.ts b/server/src/immich/controllers/dto/uuid-param.dto.ts deleted file mode 100644 index 6e1b5a36c..000000000 --- a/server/src/immich/controllers/dto/uuid-param.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsUUID } from 'class-validator'; - -export class UUIDParamDto { - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; -} diff --git a/server/src/immich/controllers/index.ts b/server/src/immich/controllers/index.ts deleted file mode 100644 index f4e473091..000000000 --- a/server/src/immich/controllers/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export * from './activity.controller'; -export * from './album.controller'; -export * from './api-key.controller'; -export * from './app.controller'; -export * from './asset.controller'; -export * from './audit.controller'; -export * from './auth.controller'; -export * from './download.controller'; -export * from './face.controller'; -export * from './job.controller'; -export * from './library.controller'; -export * from './oauth.controller'; -export * from './partner.controller'; -export * from './person.controller'; -export * from './search.controller'; -export * from './server-info.controller'; -export * from './shared-link.controller'; -export * from './system-config.controller'; -export * from './tag.controller'; -export * from './trash.controller'; -export * from './user.controller'; diff --git a/server/src/immich/index.ts b/server/src/immich/index.ts deleted file mode 100644 index c590fa83e..000000000 --- a/server/src/immich/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './app.module'; -export * from './controllers'; diff --git a/server/src/immich/interceptors/index.ts b/server/src/immich/interceptors/index.ts deleted file mode 100644 index 5811b3232..000000000 --- a/server/src/immich/interceptors/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './error.interceptor'; -export * from './file-upload.interceptor'; diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts deleted file mode 100644 index 60e323e6a..000000000 --- a/server/src/immich/main.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { WEB_ROOT, envName, isDev, serverVersion } from '@app/domain'; -import { WebSocketAdapter, excludePaths } from '@app/infra'; -import { otelSDK } from '@app/infra/instrumentation'; -import { ImmichLogger } from '@app/infra/logger'; -import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import { json } from 'body-parser'; -import cookieParser from 'cookie-parser'; -import { existsSync } from 'node:fs'; -import sirv from 'sirv'; -import { AppModule } from './app.module'; -import { AppService } from './app.service'; -import { useSwagger } from './app.utils'; - -const logger = new ImmichLogger('ImmichServer'); -const port = Number(process.env.SERVER_PORT) || 3001; - -export async function bootstrap() { - otelSDK.start(); - const app = await NestFactory.create(AppModule, { bufferLogs: true }); - - app.useLogger(app.get(ImmichLogger)); - app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); - app.set('etag', 'strong'); - app.use(cookieParser()); - app.use(json({ limit: '10mb' })); - if (isDev) { - app.enableCors(); - } - app.useWebSocketAdapter(new WebSocketAdapter(app)); - useSwagger(app, isDev); - - app.setGlobalPrefix('api', { exclude: excludePaths }); - if (existsSync(WEB_ROOT)) { - // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 - // provides serving of precompressed assets and caching of immutable assets - app.use( - sirv(WEB_ROOT, { - etag: true, - gzip: true, - brotli: true, - setHeaders: (res, pathname) => { - if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - }, - }), - ); - } - app.use(app.get(AppService).ssr(excludePaths)); - - const server = await app.listen(port); - server.requestTimeout = 30 * 60 * 1000; - - logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); -} diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts deleted file mode 100644 index af620790e..000000000 --- a/server/src/infra/entities/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ActivityEntity } from './activity.entity'; -import { AlbumEntity } from './album.entity'; -import { APIKeyEntity } from './api-key.entity'; -import { AssetFaceEntity } from './asset-face.entity'; -import { AssetJobStatusEntity } from './asset-job-status.entity'; -import { AssetStackEntity } from './asset-stack.entity'; -import { AssetEntity } from './asset.entity'; -import { AuditEntity } from './audit.entity'; -import { ExifEntity } from './exif.entity'; -import { GeodataPlacesEntity } from './geodata-places.entity'; -import { LibraryEntity } from './library.entity'; -import { MoveEntity } from './move.entity'; -import { PartnerEntity } from './partner.entity'; -import { PersonEntity } from './person.entity'; -import { SharedLinkEntity } from './shared-link.entity'; -import { SmartInfoEntity } from './smart-info.entity'; -import { SmartSearchEntity } from './smart-search.entity'; -import { SystemConfigEntity } from './system-config.entity'; -import { SystemMetadataEntity } from './system-metadata.entity'; -import { TagEntity } from './tag.entity'; -import { UserTokenEntity } from './user-token.entity'; -import { UserEntity } from './user.entity'; - -export * from './activity.entity'; -export * from './album.entity'; -export * from './api-key.entity'; -export * from './asset-face.entity'; -export * from './asset-job-status.entity'; -export * from './asset-stack.entity'; -export * from './asset.entity'; -export * from './audit.entity'; -export * from './exif.entity'; -export * from './geodata-places.entity'; -export * from './library.entity'; -export * from './move.entity'; -export * from './partner.entity'; -export * from './person.entity'; -export * from './shared-link.entity'; -export * from './smart-info.entity'; -export * from './smart-search.entity'; -export * from './system-config.entity'; -export * from './system-metadata.entity'; -export * from './tag.entity'; -export * from './user-token.entity'; -export * from './user.entity'; - -export const databaseEntities = [ - ActivityEntity, - AlbumEntity, - APIKeyEntity, - AssetEntity, - AssetStackEntity, - AssetFaceEntity, - AssetJobStatusEntity, - AuditEntity, - ExifEntity, - GeodataPlacesEntity, - MoveEntity, - PartnerEntity, - PersonEntity, - SharedLinkEntity, - SmartInfoEntity, - SmartSearchEntity, - SystemConfigEntity, - SystemMetadataEntity, - TagEntity, - UserEntity, - UserTokenEntity, - LibraryEntity, -]; diff --git a/server/src/infra/index.ts b/server/src/infra/index.ts deleted file mode 100644 index 6a218d81c..000000000 --- a/server/src/infra/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './database.config'; -export * from './infra.config'; -export * from './infra.module'; -export * from './websocket.adapter'; diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts deleted file mode 100644 index 9ea570953..000000000 --- a/server/src/infra/infra.config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { QueueName } from '@app/domain'; -import { RegisterQueueOptions } from '@nestjs/bullmq'; -import { QueueOptions } from 'bullmq'; -import { RedisOptions } from 'ioredis'; - -function parseRedisConfig(): RedisOptions { - const redisUrl = process.env.REDIS_URL; - if (redisUrl && redisUrl.startsWith('ioredis://')) { - try { - const decodedString = Buffer.from(redisUrl.slice(10), 'base64').toString(); - return JSON.parse(decodedString); - } catch (error) { - throw new Error(`Failed to decode redis options: ${error}`); - } - } - return { - host: process.env.REDIS_HOSTNAME || 'immich_redis', - port: Number.parseInt(process.env.REDIS_PORT || '6379'), - db: Number.parseInt(process.env.REDIS_DBINDEX || '0'), - username: process.env.REDIS_USERNAME || undefined, - password: process.env.REDIS_PASSWORD || undefined, - path: process.env.REDIS_SOCKET || undefined, - }; -} - -export const bullConfig: QueueOptions = { - prefix: 'immich_bull', - connection: parseRedisConfig(), - defaultJobOptions: { - attempts: 3, - removeOnComplete: true, - removeOnFail: false, - }, -}; - -export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name })); - -export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico']; diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts deleted file mode 100644 index df3773deb..000000000 --- a/server/src/infra/infra.module.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - IAccessRepository, - IActivityRepository, - IAlbumRepository, - IAssetRepository, - IAssetStackRepository, - IAuditRepository, - ICommunicationRepository, - ICryptoRepository, - IDatabaseRepository, - IJobRepository, - IKeyRepository, - ILibraryRepository, - IMachineLearningRepository, - IMediaRepository, - IMetadataRepository, - IMoveRepository, - IPartnerRepository, - IPersonRepository, - ISearchRepository, - IServerInfoRepository, - ISharedLinkRepository, - IStorageRepository, - ISystemConfigRepository, - ISystemMetadataRepository, - ITagRepository, - IUserRepository, - IUserTokenRepository, - immichAppConfig, -} from '@app/domain'; -import { BullModule } from '@nestjs/bullmq'; -import { Global, Module, Provider } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { OpenTelemetryModule } from 'nestjs-otel'; -import { databaseConfig } from './database.config'; -import { databaseEntities } from './entities'; -import { bullConfig, bullQueues } from './infra.config'; -import { otelConfig } from './instrumentation'; -import { - AccessRepository, - ActivityRepository, - AlbumRepository, - ApiKeyRepository, - AssetRepository, - AssetStackRepository, - AuditRepository, - CommunicationRepository, - CryptoRepository, - DatabaseRepository, - FilesystemProvider, - JobRepository, - LibraryRepository, - MachineLearningRepository, - MediaRepository, - MetadataRepository, - MoveRepository, - PartnerRepository, - PersonRepository, - SearchRepository, - ServerInfoRepository, - SharedLinkRepository, - SystemConfigRepository, - SystemMetadataRepository, - TagRepository, - UserRepository, - UserTokenRepository, -} from './repositories'; - -const providers: Provider[] = [ - { provide: IActivityRepository, useClass: ActivityRepository }, - { provide: IAccessRepository, useClass: AccessRepository }, - { provide: IAlbumRepository, useClass: AlbumRepository }, - { provide: IAssetRepository, useClass: AssetRepository }, - { provide: IAssetStackRepository, useClass: AssetStackRepository }, - { provide: IAuditRepository, useClass: AuditRepository }, - { provide: ICommunicationRepository, useClass: CommunicationRepository }, - { provide: ICryptoRepository, useClass: CryptoRepository }, - { provide: IDatabaseRepository, useClass: DatabaseRepository }, - { provide: IJobRepository, useClass: JobRepository }, - { provide: ILibraryRepository, useClass: LibraryRepository }, - { provide: IKeyRepository, useClass: ApiKeyRepository }, - { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, - { provide: IMetadataRepository, useClass: MetadataRepository }, - { provide: IMoveRepository, useClass: MoveRepository }, - { provide: IPartnerRepository, useClass: PartnerRepository }, - { provide: IPersonRepository, useClass: PersonRepository }, - { provide: IServerInfoRepository, useClass: ServerInfoRepository }, - { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, - { provide: ISearchRepository, useClass: SearchRepository }, - { provide: IStorageRepository, useClass: FilesystemProvider }, - { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, - { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, - { provide: ITagRepository, useClass: TagRepository }, - { provide: IMediaRepository, useClass: MediaRepository }, - { provide: IUserRepository, useClass: UserRepository }, - { provide: IUserTokenRepository, useClass: UserTokenRepository }, - SchedulerRegistry, -]; - -@Global() -@Module({ - imports: [ - ConfigModule.forRoot(immichAppConfig), - EventEmitterModule.forRoot(), - TypeOrmModule.forRoot(databaseConfig), - TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, - BullModule.forRoot(bullConfig), - BullModule.registerQueue(...bullQueues), - OpenTelemetryModule.forRoot(otelConfig), - ], - providers: [...providers], - exports: [...providers, BullModule], -}) -export class InfraModule {} - -@Global() -@Module({ - imports: [ - ConfigModule.forRoot(immichAppConfig), - EventEmitterModule.forRoot(), - TypeOrmModule.forRoot(databaseConfig), - TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, - ], - providers: [...providers], - exports: [...providers], -}) -export class InfraTestModule {} diff --git a/server/src/infra/infra.util.ts b/server/src/infra/infra.util.ts deleted file mode 100644 index 585d058e0..000000000 --- a/server/src/infra/infra.util.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; - -export const GENERATE_SQL_KEY = 'generate-sql-key'; - -export interface GenerateSqlQueries { - name?: string; - params: unknown[]; -} - -/** Decorator to enable versioning/tracking of generated Sql */ -export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); - -const UUID = '00000000-0000-4000-a000-000000000000'; - -export const DummyValue = { - UUID, - UUID_SET: new Set([UUID]), - PAGINATION: { take: 10, skip: 0 }, - EMAIL: 'user@immich.app', - STRING: 'abcdefghi', - BUFFER: Buffer.from('abcdefghi'), - DATE: new Date(), - TIME_BUCKET: '2024-01-01T00:00:00.000Z', -}; - -// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the -// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching -// by a list of IDs) requires splitting the query into multiple chunks. -// We are rounding down this limit, as queries commonly include other filters and parameters. -export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500; diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts deleted file mode 100644 index 247244108..000000000 --- a/server/src/infra/infra.utils.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/domain'; -import _ from 'lodash'; -import { - Between, - FindManyOptions, - IsNull, - LessThanOrEqual, - MoreThanOrEqual, - Not, - ObjectLiteral, - Repository, - SelectQueryBuilder, -} from 'typeorm'; -import { PaginatedBuilderOptions, PaginationMode, PaginationResult, chunks, setUnion } from '../domain/domain.util'; -import { AssetEntity } from './entities'; -import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util'; - -/** - * 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 isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { - const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; - return Number.isInteger(value) && value >= min && value <= max; -}; - -function paginationHelper(items: Entity[], take: number): PaginationResult { - const hasNextPage = items.length > take; - items.splice(take); - - return { items, hasNextPage }; -} - -export async function paginate( - repository: Repository, - { take, skip }: PaginationOptions, - searchOptions?: FindManyOptions, -): Paginated { - const items = await repository.find( - _.omitBy( - { - ...searchOptions, - // Take one more item to check if there's a next page - take: take + 1, - skip, - }, - _.isUndefined, - ), - ); - - return paginationHelper(items, take); -} - -export async function paginatedBuilder( - qb: SelectQueryBuilder, - { take, skip, mode }: PaginatedBuilderOptions, -): Paginated { - if (mode === PaginationMode.LIMIT_OFFSET) { - qb.limit(take + 1).offset(skip); - } else { - qb.take(take + 1).skip(skip); - } - - const items = await qb.getMany(); - return paginationHelper(items, take); -} - -export const asVector = (embedding: number[], quote = false) => - quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; - -/** - * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection, - * to overcome the maximum number of parameters allowed by the database driver. - * - * @param options.paramIndex The index of the function parameter to chunk. Defaults to 0. - * @param options.flatten Whether to flatten the results. Defaults to false. - */ -export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator { - return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - const parameterIndex = options.paramIndex ?? 0; - descriptor.value = async function (...arguments_: any[]) { - const argument = arguments_[parameterIndex]; - - // Early return if argument length is less than or equal to the chunk size. - if ( - (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) || - (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE) - ) { - return await originalMethod.apply(this, arguments_); - } - - return Promise.all( - chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => { - await Reflect.apply(originalMethod, this, [ - ...arguments_.slice(0, parameterIndex), - chunk, - ...arguments_.slice(parameterIndex + 1), - ]); - }), - ).then((results) => (options.mergeFn ? options.mergeFn(results) : results)); - }; - }; -} - -export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator { - return Chunked({ ...options, mergeFn: _.flatten }); -} - -export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator { - return Chunked({ ...options, mergeFn: setUnion }); -} - -// https://stackoverflow.com/a/74898678 -export function DecorateAll( - decorator: ( - target: any, - propertyKey: string, - descriptor: TypedPropertyDescriptor, - ) => TypedPropertyDescriptor | void, -) { - return (target: any) => { - const descriptors = Object.getOwnPropertyDescriptors(target.prototype); - for (const [propName, descriptor] of Object.entries(descriptors)) { - const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor'; - if (!isMethod) { - continue; - } - decorator({ ...target, constructor: { ...target.constructor, name: target.name } as any }, propName, descriptor); - Object.defineProperty(target.prototype, propName, descriptor); - } - }; -} - -export function searchAssetBuilder( - builder: SelectQueryBuilder, - options: AssetSearchBuilderOptions, -): SelectQueryBuilder { - builder.andWhere( - _.omitBy( - { - createdAt: OptionalBetween(options.createdAfter, options.createdBefore), - updatedAt: OptionalBetween(options.updatedAfter, options.updatedBefore), - deletedAt: OptionalBetween(options.trashedAfter, options.trashedBefore), - fileCreatedAt: OptionalBetween(options.takenAfter, options.takenBefore), - }, - _.isUndefined, - ), - ); - - const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined); - const hasExifQuery = Object.keys(exifInfo).length > 0; - - if (options.withExif && !hasExifQuery) { - builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo'); - } - - if (hasExifQuery) { - options.withExif - ? builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo') - : builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); - - builder.andWhere({ exifInfo }); - } - - const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']); - builder.andWhere(_.omitBy(id, _.isUndefined)); - - if (options.userIds) { - builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); - } - - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'resizePath', 'webpPath']); - builder.andWhere(_.omitBy(path, _.isUndefined)); - - if (options.originalFileName) { - builder.andWhere(`f_unaccent(${builder.alias}.originalFileName) ILIKE f_unaccent(:originalFileName)`, { - originalFileName: `%${options.originalFileName}%`, - }); - } - - const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']); - const { - isArchived, - isEncoded, - isMotion, - withArchived, - isNotInAlbum, - withFaces, - withPeople, - withSmartInfo, - personIds, - withExif, - withStacked, - trashedAfter, - trashedBefore, - } = options; - builder.andWhere( - _.omitBy( - { - ...status, - isArchived: isArchived ?? (withArchived ? undefined : false), - encodedVideoPath: isEncoded ? Not(IsNull()) : undefined, - livePhotoVideoId: isMotion ? Not(IsNull()) : undefined, - }, - _.isUndefined, - ), - ); - - if (isNotInAlbum) { - builder - .leftJoin(`${builder.alias}.albums`, 'albums') - .andWhere('albums.id IS NULL') - .andWhere(`${builder.alias}.isVisible = true`); - } - - if (withFaces || withPeople) { - builder.leftJoinAndSelect(`${builder.alias}.faces`, 'faces'); - } - - if (withPeople) { - builder.leftJoinAndSelect(`${builder.alias}.person`, 'person'); - } - - if (withSmartInfo) { - builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo'); - } - - if (personIds && personIds.length > 0) { - builder - .leftJoin(`${builder.alias}.faces`, 'faces') - .andWhere('faces.personId IN (:...personIds)', { personIds }) - .addGroupBy(`${builder.alias}.id`) - .having('COUNT(DISTINCT faces.personId) = :personCount', { personCount: personIds.length }); - - if (withExif) { - builder.addGroupBy('exifInfo.assetId'); - } - } - - if (withStacked) { - builder.leftJoinAndSelect(`${builder.alias}.stack`, 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets'); - } - - const withDeleted = options.withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); - if (withDeleted) { - builder.withDeleted(); - } - - return builder; -} diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts deleted file mode 100644 index 6429b6e19..000000000 --- a/server/src/infra/repositories/communication.repository.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - AuthService, - ClientEvent, - ICommunicationRepository, - InternalEventMap, - OnConnectCallback, - OnServerEventCallback, - ServerEvent, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { - OnGatewayConnection, - OnGatewayDisconnect, - OnGatewayInit, - WebSocketGateway, - WebSocketServer, -} from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { Instrumentation } from '../instrumentation'; - -@Instrumentation() -@WebSocketGateway({ - cors: true, - path: '/api/socket.io', - transports: ['websocket'], -}) -export class CommunicationRepository - implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, ICommunicationRepository -{ - private logger = new ImmichLogger(CommunicationRepository.name); - private onConnectCallbacks: OnConnectCallback[] = []; - private onServerEventCallbacks: Record = { - [ServerEvent.CONFIG_UPDATE]: [], - }; - - @WebSocketServer() - private server?: Server; - - constructor( - private authService: AuthService, - private eventEmitter: EventEmitter2, - ) {} - - afterInit(server: Server) { - this.logger.log('Initialized websocket server'); - - for (const event of Object.values(ServerEvent)) { - server.on(event, async () => { - this.logger.debug(`Server event: ${event} (receive)`); - const callbacks = this.onServerEventCallbacks[event]; - for (const callback of callbacks) { - await callback(); - } - }); - } - } - - on(event: 'connect' | ServerEvent, callback: OnConnectCallback | OnServerEventCallback) { - switch (event) { - case 'connect': { - this.onConnectCallbacks.push(callback); - break; - } - - default: { - this.onServerEventCallbacks[event].push(callback as OnServerEventCallback); - break; - } - } - } - - async handleConnection(client: Socket) { - try { - this.logger.log(`Websocket Connect: ${client.id}`); - const auth = await this.authService.validate(client.request.headers, {}); - await client.join(auth.user.id); - for (const callback of this.onConnectCallbacks) { - await callback(auth.user.id); - } - } catch (error: Error | any) { - this.logger.error(`Websocket connection error: ${error}`, error?.stack); - client.emit('error', 'unauthorized'); - client.disconnect(); - } - } - - async handleDisconnect(client: Socket) { - this.logger.log(`Websocket Disconnect: ${client.id}`); - await client.leave(client.nsp.name); - } - - send(event: ClientEvent, userId: string, data: any) { - this.server?.to(userId).emit(event, data); - } - - broadcast(event: ClientEvent, data: any) { - this.server?.emit(event, data); - } - - sendServerEvent(event: ServerEvent) { - this.logger.debug(`Server event: ${event} (send)`); - this.server?.serverSideEmit(event); - } - - emit(event: E, data: InternalEventMap[E]): boolean { - return this.eventEmitter.emit(event, data); - } - - emitAsync(event: E, data: InternalEventMap[E]): Promise { - return this.eventEmitter.emitAsync(event, data) as Promise; - } -} diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts deleted file mode 100644 index d684f6b00..000000000 --- a/server/src/infra/repositories/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export * from './access.repository'; -export * from './activity.repository'; -export * from './album.repository'; -export * from './api-key.repository'; -export * from './asset-stack.repository'; -export * from './asset.repository'; -export * from './audit.repository'; -export * from './communication.repository'; -export * from './crypto.repository'; -export * from './database.repository'; -export * from './filesystem.provider'; -export * from './job.repository'; -export * from './library.repository'; -export * from './machine-learning.repository'; -export * from './media.repository'; -export * from './metadata.repository'; -export * from './move.repository'; -export * from './partner.repository'; -export * from './person.repository'; -export * from './search.repository'; -export * from './server-info.repository'; -export * from './shared-link.repository'; -export * from './system-config.repository'; -export * from './system-metadata.repository'; -export * from './tag.repository'; -export * from './user-token.repository'; -export * from './user.repository'; diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts deleted file mode 100644 index 6f3c298c0..000000000 --- a/server/src/infra/sql-generator/sql.logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { format } from 'sql-formatter'; -import { Logger } from 'typeorm'; - -export class SqlLogger implements Logger { - queries: string[] = []; - errors: Array<{ error: string | Error; query: string }> = []; - - clear() { - this.queries = []; - this.errors = []; - } - - logQuery(query: string) { - this.queries.push(format(query, { language: 'postgresql' })); - } - - logQueryError(error: string | Error, query: string) { - this.errors.push({ error, query }); - } - - logQuerySlow() {} - logSchemaBuild() {} - logMigration() {} - log() {} -} diff --git a/server/src/infra/sql/audit.repository.sql b/server/src/infra/sql/audit.repository.sql deleted file mode 100644 index 21f9f116b..000000000 --- a/server/src/infra/sql/audit.repository.sql +++ /dev/null @@ -1 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/partner.repository.sql b/server/src/infra/sql/partner.repository.sql deleted file mode 100644 index 21f9f116b..000000000 --- a/server/src/infra/sql/partner.repository.sql +++ /dev/null @@ -1 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/system.metadata.repository.sql b/server/src/infra/sql/system.metadata.repository.sql deleted file mode 100644 index 21f9f116b..000000000 --- a/server/src/infra/sql/system.metadata.repository.sql +++ /dev/null @@ -1 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/tag.repository.sql b/server/src/infra/sql/tag.repository.sql deleted file mode 100644 index 21f9f116b..000000000 --- a/server/src/infra/sql/tag.repository.sql +++ /dev/null @@ -1 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/interfaces/access.interface.ts similarity index 94% rename from server/src/domain/repositories/access.repository.ts rename to server/src/interfaces/access.interface.ts index 7924a29dd..8b9bdcc4b 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/interfaces/access.interface.ts @@ -32,6 +32,10 @@ export interface IAccessRepository { checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; + memory: { + checkOwnerAccess(userId: string, memoryIds: Set): Promise>; + }; + person: { checkFaceOwnerAccess(userId: string, assetFaceId: Set): Promise>; checkOwnerAccess(userId: string, personIds: Set): Promise>; diff --git a/server/src/domain/repositories/activity.repository.ts b/server/src/interfaces/activity.interface.ts similarity index 72% rename from server/src/domain/repositories/activity.repository.ts rename to server/src/interfaces/activity.interface.ts index 6f5476a28..5e7b4d2c7 100644 --- a/server/src/domain/repositories/activity.repository.ts +++ b/server/src/interfaces/activity.interface.ts @@ -1,5 +1,5 @@ -import { ActivityEntity } from '@app/infra/entities/activity.entity'; -import { ActivitySearch } from '@app/infra/repositories'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { ActivitySearch } from 'src/repositories/activity.repository'; export const IActivityRepository = 'IActivityRepository'; diff --git a/server/src/domain/repositories/album.repository.ts b/server/src/interfaces/album.interface.ts similarity index 86% rename from server/src/domain/repositories/album.repository.ts rename to server/src/interfaces/album.interface.ts index eb4d4bf3d..48c728feb 100644 --- a/server/src/domain/repositories/album.repository.ts +++ b/server/src/interfaces/album.interface.ts @@ -1,4 +1,5 @@ -import { AlbumEntity } from '@app/infra/entities'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { IBulkAsset } from 'src/utils/asset.util'; export const IAlbumRepository = 'IAlbumRepository'; @@ -23,15 +24,14 @@ export interface AlbumAssets { assetIds: string[]; } -export interface IAlbumRepository { +export interface IAlbumRepository extends IBulkAsset { getById(id: string, options: AlbumInfoOptions): Promise; getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; - addAssets(assets: AlbumAssets): Promise; getAssetIds(albumId: string, assetIds?: string[]): Promise>; hasAsset(asset: AlbumAsset): Promise; removeAsset(assetId: string): Promise; - removeAssets(albumId: string, assetIds: string[]): Promise; + removeAssetIds(albumId: string, assetIds: string[]): Promise; getMetadataForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; diff --git a/server/src/domain/repositories/api-key.repository.ts b/server/src/interfaces/api-key.interface.ts similarity index 90% rename from server/src/domain/repositories/api-key.repository.ts rename to server/src/interfaces/api-key.interface.ts index 60f26f235..731b7ff6f 100644 --- a/server/src/domain/repositories/api-key.repository.ts +++ b/server/src/interfaces/api-key.interface.ts @@ -1,4 +1,4 @@ -import { APIKeyEntity } from '@app/infra/entities'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; export const IKeyRepository = 'IKeyRepository'; diff --git a/server/src/domain/repositories/asset-stack.repository.ts b/server/src/interfaces/asset-stack.interface.ts similarity index 83% rename from server/src/domain/repositories/asset-stack.repository.ts rename to server/src/interfaces/asset-stack.interface.ts index 66201ea3a..1e037d38e 100644 --- a/server/src/domain/repositories/asset-stack.repository.ts +++ b/server/src/interfaces/asset-stack.interface.ts @@ -1,4 +1,4 @@ -import { AssetStackEntity } from '@app/infra/entities/asset-stack.entity'; +import { AssetStackEntity } from 'src/entities/asset-stack.entity'; export const IAssetStackRepository = 'IAssetStackRepository'; diff --git a/server/src/interfaces/asset-v1.interface.ts b/server/src/interfaces/asset-v1.interface.ts new file mode 100644 index 000000000..8348bfaee --- /dev/null +++ b/server/src/interfaces/asset-v1.interface.ts @@ -0,0 +1,25 @@ +import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; + +export interface AssetCheck { + id: string; + checksum: Buffer; +} + +export interface AssetOwnerCheck extends AssetCheck { + ownerId: string; +} + +export interface IAssetRepositoryV1 { + get(id: string): Promise; + getLocationsByUserId(userId: string): Promise; + getDetectedObjectsByUserId(userId: string): Promise; + getAllByUserId(userId: string, dto: AssetSearchDto): Promise; + getSearchPropertiesByUserId(userId: string): Promise; + getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; + getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; + getByOriginalPath(originalPath: string): Promise; +} + +export const IAssetRepositoryV1 = 'IAssetRepositoryV1'; diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/interfaces/asset.interface.ts similarity index 82% rename from server/src/domain/repositories/asset.repository.ts rename to server/src/interfaces/asset.interface.ts index c4ddb3107..008931566 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,11 @@ -import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain'; -import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity } from '@app/infra/entities'; +import { AssetOrder } from 'src/entities/album.entity'; +import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { ReverseGeocodeResult } from 'src/interfaces/metadata.interface'; +import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; +import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; -import { Paginated, PaginationOptions } from '../domain.util'; export type AssetStats = Record; @@ -90,6 +94,25 @@ export type AssetCreate = Pick< > & Partial; +export type AssetWithoutRelations = Omit< + AssetEntity, + | 'livePhotoVideo' + | 'stack' + | 'albums' + | 'faces' + | 'owner' + | 'library' + | 'exifInfo' + | 'sharedLinks' + | 'smartInfo' + | 'smartSearch' + | 'tags' +>; + +export type AssetUpdateOptions = Pick & Partial; + +export type AssetUpdateAllOptions = Omit, 'id'>; + export interface MonthDay { day: number; month: number; @@ -116,7 +139,6 @@ export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { create(asset: AssetCreate): Promise; - getByDate(ownerId: string, date: Date): Promise; getByIds( ids: string[], relations?: FindOptionsRelations, @@ -138,8 +160,8 @@ export interface IAssetRepository { deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; getAllByDeviceId(userId: string, deviceId: string): Promise; - updateAll(ids: string[], options: Partial): Promise; - save(asset: Pick & Partial): Promise; + updateAll(ids: string[], options: Partial): Promise; + update(asset: AssetUpdateOptions): Promise; remove(asset: AssetEntity): Promise; softDeleteAll(ids: string[]): Promise; restoreAll(ids: string[]): Promise; diff --git a/server/src/domain/repositories/audit.repository.ts b/server/src/interfaces/audit.interface.ts similarity index 78% rename from server/src/domain/repositories/audit.repository.ts rename to server/src/interfaces/audit.interface.ts index 774ab1e42..767a4bc2f 100644 --- a/server/src/domain/repositories/audit.repository.ts +++ b/server/src/interfaces/audit.interface.ts @@ -1,4 +1,4 @@ -import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities'; +import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; export const IAuditRepository = 'IAuditRepository'; diff --git a/server/src/domain/repositories/crypto.repository.ts b/server/src/interfaces/crypto.interface.ts similarity index 92% rename from server/src/domain/repositories/crypto.repository.ts rename to server/src/interfaces/crypto.interface.ts index c33ee9cd7..e7ad2b045 100644 --- a/server/src/domain/repositories/crypto.repository.ts +++ b/server/src/interfaces/crypto.interface.ts @@ -8,4 +8,5 @@ export interface ICryptoRepository { hashSha1(data: string | Buffer): Buffer; hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise; compareBcrypt(data: string | Buffer, encrypted: string): boolean; + newPassword(bytes: number): string; } diff --git a/server/src/domain/repositories/database.repository.ts b/server/src/interfaces/database.interface.ts similarity index 97% rename from server/src/domain/repositories/database.repository.ts rename to server/src/interfaces/database.interface.ts index 55911e7ce..42342eccc 100644 --- a/server/src/domain/repositories/database.repository.ts +++ b/server/src/interfaces/database.interface.ts @@ -1,4 +1,4 @@ -import { Version } from '../domain.constant'; +import { Version } from 'src/utils/version'; export enum DatabaseExtension { CUBE = 'cube', diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts new file mode 100644 index 000000000..49b5177f3 --- /dev/null +++ b/server/src/interfaces/event.interface.ts @@ -0,0 +1,72 @@ +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server-info.dto'; +import { SystemConfig } from 'src/entities/system-config.entity'; + +export const IEventRepository = 'IEventRepository'; + +export enum ClientEvent { + UPLOAD_SUCCESS = 'on_upload_success', + USER_DELETE = 'on_user_delete', + ASSET_DELETE = 'on_asset_delete', + ASSET_TRASH = 'on_asset_trash', + ASSET_UPDATE = 'on_asset_update', + ASSET_HIDDEN = 'on_asset_hidden', + ASSET_RESTORE = 'on_asset_restore', + ASSET_STACK_UPDATE = 'on_asset_stack_update', + PERSON_THUMBNAIL = 'on_person_thumbnail', + SERVER_VERSION = 'on_server_version', + CONFIG_UPDATE = 'on_config_update', + NEW_RELEASE = 'on_new_release', +} + +export interface ClientEventMap { + [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; + [ClientEvent.USER_DELETE]: string; + [ClientEvent.ASSET_DELETE]: string; + [ClientEvent.ASSET_TRASH]: string[]; + [ClientEvent.ASSET_UPDATE]: AssetResponseDto; + [ClientEvent.ASSET_HIDDEN]: string; + [ClientEvent.ASSET_RESTORE]: string[]; + [ClientEvent.ASSET_STACK_UPDATE]: string[]; + [ClientEvent.PERSON_THUMBNAIL]: string; + [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; + [ClientEvent.CONFIG_UPDATE]: Record; + [ClientEvent.NEW_RELEASE]: ReleaseNotification; +} + +export enum ServerEvent { + CONFIG_UPDATE = 'config.update', + WEBSOCKET_CONNECT = 'websocket.connect', +} + +export interface ServerEventMap { + [ServerEvent.CONFIG_UPDATE]: null; + [ServerEvent.WEBSOCKET_CONNECT]: { userId: string }; +} + +export enum ServerAsyncEvent { + CONFIG_VALIDATE = 'config.validate', +} + +export interface ServerAsyncEventMap { + [ServerAsyncEvent.CONFIG_VALIDATE]: { newConfig: SystemConfig; oldConfig: SystemConfig }; +} + +export interface IEventRepository { + /** + * Send to connected clients for a specific user + */ + clientSend(event: E, userId: string, data: ClientEventMap[E]): void; + /** + * Send to all connected clients + */ + clientBroadcast(event: E, data: ClientEventMap[E]): void; + /** + * Notify listeners in this and connected processes. Subscribe to an event with `@OnServerEvent` + */ + serverSend(event: E, data: ServerEventMap[E]): boolean; + /** + * Notify and wait for responses from listeners in this process. Subscribe to an event with `@OnServerEvent` + */ + serverSendAsync(event: E, data: ServerAsyncEventMap[E]): Promise; +} diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/interfaces/job.interface.ts similarity index 50% rename from server/src/domain/repositories/job.repository.ts rename to server/src/interfaces/job.interface.ts index 3d31dd16b..eddaefcf3 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/interfaces/job.interface.ts @@ -1,15 +1,139 @@ -import { JobName, QueueName } from '../job/job.constants'; +export enum QueueName { + THUMBNAIL_GENERATION = 'thumbnailGeneration', + METADATA_EXTRACTION = 'metadataExtraction', + VIDEO_CONVERSION = 'videoConversion', + FACE_DETECTION = 'faceDetection', + FACIAL_RECOGNITION = 'facialRecognition', + SMART_SEARCH = 'smartSearch', + BACKGROUND_TASK = 'backgroundTask', + STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', + MIGRATION = 'migration', + SEARCH = 'search', + SIDECAR = 'sidecar', + LIBRARY = 'library', +} -import { - IAssetDeletionJob, - IBaseJob, - IDeferrableJob, - IDeleteFilesJob, - IEntityJob, - ILibraryFileJob, - ILibraryRefreshJob, - ISidecarWriteJob, -} from '../job/job.interface'; +export type ConcurrentQueueName = Exclude< + QueueName, + QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION +>; + +export enum JobCommand { + START = 'start', + PAUSE = 'pause', + RESUME = 'resume', + EMPTY = 'empty', + CLEAR_FAILED = 'clear-failed', +} + +export enum JobName { + // conversion + QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', + VIDEO_CONVERSION = 'video-conversion', + + // thumbnails + QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', + GENERATE_PREVIEW = 'generate-preview', + GENERATE_THUMBNAIL = 'generate-thumbnail', + GENERATE_THUMBHASH = 'generate-thumbhash', + GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', + + // metadata + QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', + METADATA_EXTRACTION = 'metadata-extraction', + LINK_LIVE_PHOTOS = 'link-live-photos', + + // user + USER_DELETION = 'user-deletion', + USER_DELETE_CHECK = 'user-delete-check', + USER_SYNC_USAGE = 'user-sync-usage', + + // asset + ASSET_DELETION = 'asset-deletion', + ASSET_DELETION_CHECK = 'asset-deletion-check', + + // storage template + STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', + STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', + + // migration + QUEUE_MIGRATION = 'queue-migration', + MIGRATE_ASSET = 'migrate-asset', + MIGRATE_PERSON = 'migrate-person', + + // facial recognition + PERSON_CLEANUP = 'person-cleanup', + QUEUE_FACE_DETECTION = 'queue-face-detection', + FACE_DETECTION = 'face-detection', + QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', + FACIAL_RECOGNITION = 'facial-recognition', + + // library management + LIBRARY_SCAN = 'library-refresh', + LIBRARY_SCAN_ASSET = 'library-refresh-asset', + LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', + LIBRARY_DELETE = 'library-delete', + LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', + LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', + + // cleanup + DELETE_FILES = 'delete-files', + CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', + + // smart search + QUEUE_SMART_SEARCH = 'queue-smart-search', + SMART_SEARCH = 'smart-search', + + // XMP sidecars + QUEUE_SIDECAR = 'queue-sidecar', + SIDECAR_DISCOVERY = 'sidecar-discovery', + SIDECAR_SYNC = 'sidecar-sync', + SIDECAR_WRITE = 'sidecar-write', +} + +export const JOBS_ASSET_PAGINATION_SIZE = 1000; + +export interface IBaseJob { + force?: boolean; +} + +export interface IEntityJob extends IBaseJob { + id: string; + source?: 'upload' | 'sidecar-write'; +} + +export interface IAssetDeletionJob extends IEntityJob { + fromExternal?: boolean; +} + +export interface ILibraryFileJob extends IEntityJob { + ownerId: string; + assetPath: string; +} + +export interface ILibraryRefreshJob extends IEntityJob { + refreshModifiedFiles: boolean; + refreshAllFiles: boolean; +} + +export interface IBulkEntityJob extends IBaseJob { + ids: string[]; +} + +export interface IDeleteFilesJob extends IBaseJob { + files: Array; +} + +export interface ISidecarWriteJob extends IEntityJob { + description?: string; + dateTimeOriginal?: string; + latitude?: number; + longitude?: number; +} + +export interface IDeferrableJob extends IEntityJob { + deferred?: boolean; +} export interface JobCounts { active: number; @@ -36,9 +160,9 @@ export type JobItem = // Thumbnails | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } - | { name: JobName.GENERATE_JPEG_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_PREVIEW; data: IEntityJob } + | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob } // User | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } diff --git a/server/src/domain/repositories/library.repository.ts b/server/src/interfaces/library.interface.ts similarity index 85% rename from server/src/domain/repositories/library.repository.ts rename to server/src/interfaces/library.interface.ts index 395373bcc..dbc7fab81 100644 --- a/server/src/domain/repositories/library.repository.ts +++ b/server/src/interfaces/library.interface.ts @@ -1,5 +1,5 @@ -import { LibraryEntity, LibraryType } from '@app/infra/entities'; -import { LibraryStatsResponseDto } from '../library/library.dto'; +import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; export const ILibraryRepository = 'ILibraryRepository'; diff --git a/server/src/domain/repositories/machine-learning.repository.ts b/server/src/interfaces/machine-learning.interface.ts similarity index 92% rename from server/src/domain/repositories/machine-learning.repository.ts rename to server/src/interfaces/machine-learning.interface.ts index f327a7a70..0aeed7635 100644 --- a/server/src/domain/repositories/machine-learning.repository.ts +++ b/server/src/interfaces/machine-learning.interface.ts @@ -1,4 +1,4 @@ -import { CLIPConfig, RecognitionConfig } from '../smart-info/dto'; +import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; export const IMachineLearningRepository = 'IMachineLearningRepository'; diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/interfaces/media.interface.ts similarity index 93% rename from server/src/domain/repositories/media.repository.ts rename to server/src/interfaces/media.interface.ts index ed6f88449..5e51e94a5 100644 --- a/server/src/domain/repositories/media.repository.ts +++ b/server/src/interfaces/media.interface.ts @@ -1,11 +1,11 @@ -import { TranscodeTarget, VideoCodec } from '@app/infra/entities'; import { Writable } from 'node:stream'; +import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; export const IMediaRepository = 'IMediaRepository'; export interface ResizeOptions { size: number; - format: 'webp' | 'jpeg'; + format: ImageFormat; colorspace: string; quality: number; } diff --git a/server/src/interfaces/memory.interface.ts b/server/src/interfaces/memory.interface.ts new file mode 100644 index 000000000..505e1662c --- /dev/null +++ b/server/src/interfaces/memory.interface.ts @@ -0,0 +1,14 @@ +import { MemoryEntity } from 'src/entities/memory.entity'; + +export const IMemoryRepository = 'IMemoryRepository'; + +export interface IMemoryRepository { + search(ownerId: string): Promise; + get(id: string): Promise; + create(memory: Partial): Promise; + update(memory: Partial): Promise; + delete(id: string): Promise; + getAssetIds(id: string, assetIds: string[]): Promise>; + addAssetIds(id: string, assetIds: string[]): Promise; + removeAssetIds(id: string, assetIds: string[]): Promise; +} diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/interfaces/metadata.interface.ts similarity index 100% rename from server/src/domain/repositories/metadata.repository.ts rename to server/src/interfaces/metadata.interface.ts diff --git a/server/src/interfaces/metric.interface.ts b/server/src/interfaces/metric.interface.ts new file mode 100644 index 000000000..a87a84983 --- /dev/null +++ b/server/src/interfaces/metric.interface.ts @@ -0,0 +1,21 @@ +import { MetricOptions } from '@opentelemetry/api'; + +export const IMetricRepository = 'IMetricRepository'; + +export interface MetricGroupOptions { + enabled: boolean; +} + +export interface IMetricGroupRepository { + addToCounter(name: string, value: number, options?: MetricOptions): void; + addToGauge(name: string, value: number, options?: MetricOptions): void; + addToHistogram(name: string, value: number, options?: MetricOptions): void; + configure(options: MetricGroupOptions): this; +} + +export interface IMetricRepository { + api: IMetricGroupRepository; + host: IMetricGroupRepository; + jobs: IMetricGroupRepository; + repo: IMetricGroupRepository; +} diff --git a/server/src/domain/repositories/move.repository.ts b/server/src/interfaces/move.interface.ts similarity index 87% rename from server/src/domain/repositories/move.repository.ts rename to server/src/interfaces/move.interface.ts index 20caa117f..c9d39e78c 100644 --- a/server/src/domain/repositories/move.repository.ts +++ b/server/src/interfaces/move.interface.ts @@ -1,4 +1,4 @@ -import { MoveEntity, PathType } from '@app/infra/entities'; +import { MoveEntity, PathType } from 'src/entities/move.entity'; export const IMoveRepository = 'IMoveRepository'; diff --git a/server/src/domain/repositories/partner.repository.ts b/server/src/interfaces/partner.interface.ts similarity index 89% rename from server/src/domain/repositories/partner.repository.ts rename to server/src/interfaces/partner.interface.ts index f0409b67a..842745a51 100644 --- a/server/src/domain/repositories/partner.repository.ts +++ b/server/src/interfaces/partner.interface.ts @@ -1,4 +1,4 @@ -import { PartnerEntity } from '@app/infra/entities'; +import { PartnerEntity } from 'src/entities/partner.entity'; export interface PartnerIds { sharedById: string; diff --git a/server/src/domain/repositories/person.repository.ts b/server/src/interfaces/person.interface.ts similarity index 89% rename from server/src/domain/repositories/person.repository.ts rename to server/src/interfaces/person.interface.ts index 85c11fe92..382bbda22 100644 --- a/server/src/domain/repositories/person.repository.ts +++ b/server/src/interfaces/person.interface.ts @@ -1,6 +1,8 @@ -import { AssetEntity, AssetFaceEntity, PersonEntity } from '@app/infra/entities'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; -import { Paginated, PaginationOptions } from '../domain.util'; export const IPersonRepository = 'IPersonRepository'; diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/interfaces/search.interface.ts similarity index 90% rename from server/src/domain/repositories/search.repository.ts rename to server/src/interfaces/search.interface.ts index 10182a44e..771b23e9c 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/interfaces/search.interface.ts @@ -1,5 +1,7 @@ -import { AssetEntity, AssetFaceEntity, AssetType, GeodataPlacesEntity, SmartInfoEntity } from '@app/infra/entities'; -import { Paginated } from '../domain.util'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { Paginated } from 'src/utils/pagination'; export const ISearchRepository = 'ISearchRepository'; @@ -115,8 +117,8 @@ export interface SearchPathOptions { encodedVideoPath?: string; originalFileName?: string; originalPath?: string; - resizePath?: string; - webpPath?: string; + previewPath?: string; + thumbnailPath?: string; } export interface SearchExifOptions { @@ -185,7 +187,8 @@ export interface ISearchRepository { searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated; searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; searchFaces(search: FaceEmbeddingSearch): Promise; - upsert(smartInfo: Partial, embedding?: Embedding): Promise; + upsert(assetId: string, embedding: number[]): Promise; searchPlaces(placeName: string): Promise; + getAssetsByCity(userIds: string[]): Promise; deleteAllSearchEmbeddings(): Promise; } diff --git a/server/src/domain/repositories/server-info.repository.ts b/server/src/interfaces/server-info.interface.ts similarity index 100% rename from server/src/domain/repositories/server-info.repository.ts rename to server/src/interfaces/server-info.interface.ts diff --git a/server/src/domain/repositories/shared-link.repository.ts b/server/src/interfaces/shared-link.interface.ts similarity index 87% rename from server/src/domain/repositories/shared-link.repository.ts rename to server/src/interfaces/shared-link.interface.ts index 0f0255d0a..fe08a6294 100644 --- a/server/src/domain/repositories/shared-link.repository.ts +++ b/server/src/interfaces/shared-link.interface.ts @@ -1,4 +1,4 @@ -import { SharedLinkEntity } from '@app/infra/entities'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; export const ISharedLinkRepository = 'ISharedLinkRepository'; diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/interfaces/storage.interface.ts similarity index 97% rename from server/src/domain/repositories/storage.repository.ts rename to server/src/interfaces/storage.interface.ts index a052596c0..e78bb0195 100644 --- a/server/src/domain/repositories/storage.repository.ts +++ b/server/src/interfaces/storage.interface.ts @@ -2,7 +2,7 @@ import { WatchOptions } from 'chokidar'; import { Stats } from 'node:fs'; import { FileReadOptions } from 'node:fs/promises'; import { Readable } from 'node:stream'; -import { CrawlOptionsDto } from '../library'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; export interface ImmichReadStream { stream: Readable; diff --git a/server/src/domain/repositories/system-config.repository.ts b/server/src/interfaces/system-config.interface.ts similarity index 83% rename from server/src/domain/repositories/system-config.repository.ts rename to server/src/interfaces/system-config.interface.ts index d154f6eff..f591a6671 100644 --- a/server/src/domain/repositories/system-config.repository.ts +++ b/server/src/interfaces/system-config.interface.ts @@ -1,4 +1,4 @@ -import { SystemConfigEntity } from '@app/infra/entities'; +import { SystemConfigEntity } from 'src/entities/system-config.entity'; export const ISystemConfigRepository = 'ISystemConfigRepository'; diff --git a/server/src/domain/repositories/system-metadata.repository.ts b/server/src/interfaces/system-metadata.interface.ts similarity index 80% rename from server/src/domain/repositories/system-metadata.repository.ts rename to server/src/interfaces/system-metadata.interface.ts index 4d571953b..cbbce44e2 100644 --- a/server/src/domain/repositories/system-metadata.repository.ts +++ b/server/src/interfaces/system-metadata.interface.ts @@ -1,4 +1,4 @@ -import { SystemMetadata } from '@app/infra/entities'; +import { SystemMetadata } from 'src/entities/system-metadata.entity'; export const ISystemMetadataRepository = 'ISystemMetadataRepository'; diff --git a/server/src/domain/repositories/tag.repository.ts b/server/src/interfaces/tag.interface.ts similarity index 86% rename from server/src/domain/repositories/tag.repository.ts rename to server/src/interfaces/tag.interface.ts index 4e6f583b4..8071461df 100644 --- a/server/src/domain/repositories/tag.repository.ts +++ b/server/src/interfaces/tag.interface.ts @@ -1,4 +1,5 @@ -import { AssetEntity, TagEntity } from '@app/infra/entities'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { TagEntity } from 'src/entities/tag.entity'; export const ITagRepository = 'ITagRepository'; diff --git a/server/src/domain/repositories/user-token.repository.ts b/server/src/interfaces/user-token.interface.ts similarity index 85% rename from server/src/domain/repositories/user-token.repository.ts rename to server/src/interfaces/user-token.interface.ts index 713b3f1ef..0fcec39fd 100644 --- a/server/src/domain/repositories/user-token.repository.ts +++ b/server/src/interfaces/user-token.interface.ts @@ -1,4 +1,4 @@ -import { UserTokenEntity } from '@app/infra/entities'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; export const IUserTokenRepository = 'IUserTokenRepository'; diff --git a/server/src/domain/repositories/user.repository.ts b/server/src/interfaces/user.interface.ts similarity index 95% rename from server/src/domain/repositories/user.repository.ts rename to server/src/interfaces/user.interface.ts index efd950318..ebc70688e 100644 --- a/server/src/domain/repositories/user.repository.ts +++ b/server/src/interfaces/user.interface.ts @@ -1,4 +1,4 @@ -import { UserEntity } from '@app/infra/entities'; +import { UserEntity } from 'src/entities/user.entity'; export interface UserListFilter { withDeleted?: boolean; diff --git a/server/src/main.ts b/server/src/main.ts index 198b0f087..3a9303868 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,6 +1,75 @@ -import { bootstrap as admin } from './immich-admin/main'; -import { bootstrap as server } from './immich/main'; -import { bootstrap as microservices } from './microservices/main'; +import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { json } from 'body-parser'; +import cookieParser from 'cookie-parser'; +import { CommandFactory } from 'nest-commander'; +import { existsSync } from 'node:fs'; +import sirv from 'sirv'; +import { ApiModule, ImmichAdminModule, MicroservicesModule } from 'src/app.module'; +import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; +import { LogLevel } from 'src/entities/system-config.entity'; +import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; +import { ApiService } from 'src/services/api.service'; +import { otelSDK } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { useSwagger } from 'src/utils/misc'; + +async function bootstrapMicroservices() { + const logger = new ImmichLogger('ImmichMicroservice'); + const port = Number(process.env.MICROSERVICES_PORT) || 3002; + + otelSDK.start(); + const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); + app.useLogger(app.get(ImmichLogger)); + app.useWebSocketAdapter(new WebSocketAdapter(app)); + + await app.listen(port); + + logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); +} + +async function bootstrapApi() { + const logger = new ImmichLogger('ImmichServer'); + const port = Number(process.env.SERVER_PORT) || 3001; + + otelSDK.start(); + const app = await NestFactory.create(ApiModule, { bufferLogs: true }); + + app.useLogger(app.get(ImmichLogger)); + app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); + app.set('etag', 'strong'); + app.use(cookieParser()); + app.use(json({ limit: '10mb' })); + if (isDev) { + app.enableCors(); + } + app.useWebSocketAdapter(new WebSocketAdapter(app)); + useSwagger(app, isDev); + + app.setGlobalPrefix('api', { exclude: excludePaths }); + if (existsSync(WEB_ROOT)) { + // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 + // provides serving of precompressed assets and caching of immutable assets + app.use( + sirv(WEB_ROOT, { + etag: true, + gzip: true, + brotli: true, + setHeaders: (res, pathname) => { + if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + }, + }), + ); + } + app.use(app.get(ApiService).ssr(excludePaths)); + + const server = await app.listen(port); + server.requestTimeout = 30 * 60 * 1000; + + logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); +} const immichApp = process.argv[2] || process.env.IMMICH_APP; @@ -8,23 +77,29 @@ if (process.argv[2] === immichApp) { process.argv.splice(2, 1); } +async function bootstrapImmichAdmin() { + process.env.LOG_LEVEL = LogLevel.WARN; + await CommandFactory.run(ImmichAdminModule); +} + function bootstrap() { switch (immichApp) { case 'immich': { process.title = 'immich_server'; - return server(); + return bootstrapApi(); } case 'microservices': { process.title = 'immich_microservices'; - return microservices(); + return bootstrapMicroservices(); } case 'immich-admin': { process.title = 'immich_admin_cli'; - return admin(); + return bootstrapImmichAdmin(); } default: { - throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); + throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`); } } } + void bootstrap(); diff --git a/server/src/microservices/main.ts b/server/src/microservices/main.ts deleted file mode 100644 index f7dc64f57..000000000 --- a/server/src/microservices/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { envName, serverVersion } from '@app/domain'; -import { WebSocketAdapter } from '@app/infra'; -import { otelSDK } from '@app/infra/instrumentation'; -import { ImmichLogger } from '@app/infra/logger'; -import { NestFactory } from '@nestjs/core'; -import { MicroservicesModule } from './microservices.module'; - -const logger = new ImmichLogger('ImmichMicroservice'); -const port = Number(process.env.MICROSERVICES_PORT) || 3002; - -export async function bootstrap() { - otelSDK.start(); - const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); - app.useLogger(app.get(ImmichLogger)); - app.useWebSocketAdapter(new WebSocketAdapter(app)); - - await app.listen(port); - - logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); -} diff --git a/server/src/microservices/microservices.module.ts b/server/src/microservices/microservices.module.ts deleted file mode 100644 index 4768d965f..000000000 --- a/server/src/microservices/microservices.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { DomainModule } from '@app/domain'; -import { InfraModule } from '@app/infra'; -import { Module, OnModuleInit } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Module({ - imports: [InfraModule, DomainModule], - providers: [AppService], -}) -export class MicroservicesModule implements OnModuleInit { - constructor(private appService: AppService) {} - - async onModuleInit() { - await this.appService.init(); - } -} diff --git a/server/src/microservices/utils/exif/coordinates.spec.ts b/server/src/microservices/utils/exif/coordinates.spec.ts deleted file mode 100644 index b9644fb49..000000000 --- a/server/src/microservices/utils/exif/coordinates.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { parseLatitude, parseLongitude } from './coordinates'; - -describe('parsing latitude from string input', () => { - it('returns null for invalid inputs', () => { - expect(parseLatitude('')).toBeNull(); - expect(parseLatitude('NaN')).toBeNull(); - expect(parseLatitude('Infinity')).toBeNull(); - expect(parseLatitude('-Infinity')).toBeNull(); - expect(parseLatitude('90.001')).toBeNull(); - expect(parseLatitude(-90.000_001)).toBeNull(); - expect(parseLatitude('1000')).toBeNull(); - expect(parseLatitude(-1000)).toBeNull(); - }); - - it('returns the numeric coordinate for valid inputs', () => { - expect(parseLatitude('90')).toBeCloseTo(90); - expect(parseLatitude('-90')).toBeCloseTo(-90); - expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999); - expect(parseLatitude('-89.9')).toBeCloseTo(-89.9); - expect(parseLatitude(0)).toBeCloseTo(0); - expect(parseLatitude('-0.0')).toBeCloseTo(-0); - }); -}); - -describe('parsing latitude from null input', () => { - it('returns null for null input', () => { - expect(parseLatitude(null)).toBeNull(); - }); -}); - -describe('parsing longitude from string input', () => { - it('returns null for invalid inputs', () => { - expect(parseLongitude('')).toBeNull(); - expect(parseLongitude('NaN')).toBeNull(); - expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull(); - expect(parseLongitude('-Infinity')).toBeNull(); - expect(parseLongitude('180.001')).toBeNull(); - expect(parseLongitude('-180.000001')).toBeNull(); - expect(parseLongitude(1000)).toBeNull(); - expect(parseLongitude('-1000')).toBeNull(); - }); - - it('returns the numeric coordinate for valid inputs', () => { - expect(parseLongitude(180)).toBeCloseTo(180); - expect(parseLongitude('-180')).toBeCloseTo(-180); - expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999); - expect(parseLongitude(-179.9)).toBeCloseTo(-179.9); - expect(parseLongitude('0')).toBeCloseTo(0); - expect(parseLongitude('-0.0')).toBeCloseTo(-0); - }); -}); - -describe('parsing longitude from null input', () => { - it('returns null for null input', () => { - expect(parseLongitude(null)).toBeNull(); - }); -}); diff --git a/server/src/microservices/utils/exif/coordinates.ts b/server/src/microservices/utils/exif/coordinates.ts deleted file mode 100644 index 03aeb17f0..000000000 --- a/server/src/microservices/utils/exif/coordinates.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { isNumberInRange } from '../numbers'; - -export function parseLatitude(input: string | number | null): number | null { - if (input === null) { - return null; - } - const latitude = typeof input === 'string' ? Number.parseFloat(input) : input; - - if (isNumberInRange(latitude, -90, 90)) { - return latitude; - } - return null; -} - -export function parseLongitude(input: string | number | null): number | null { - if (input === null) { - return null; - } - - const longitude = typeof input === 'string' ? Number.parseFloat(input) : input; - - if (isNumberInRange(longitude, -180, 180)) { - return longitude; - } - return null; -} diff --git a/server/src/microservices/utils/numbers.spec.ts b/server/src/microservices/utils/numbers.spec.ts deleted file mode 100644 index 47f95b8aa..000000000 --- a/server/src/microservices/utils/numbers.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { isDecimalNumber, isNumberInRange, toNumberOrNull } from './numbers'; - -describe('checks if a number is a decimal number', () => { - it('returns false for non-decimal numbers', () => { - expect(isDecimalNumber(Number.NaN)).toBe(false); - expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false); - expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false); - }); - - it('returns true for decimal numbers', () => { - expect(isDecimalNumber(0)).toBe(true); - expect(isDecimalNumber(-0)).toBe(true); - expect(isDecimalNumber(10.123_45)).toBe(true); - expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true); - expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true); - }); -}); - -describe('checks if a number is within a range', () => { - it('returns false for numbers outside the range', () => { - expect(isNumberInRange(0, 10, 10)).toBe(false); - expect(isNumberInRange(0.01, 10, 10)).toBe(false); - expect(isNumberInRange(50.1, 0, 50)).toBe(false); - }); - - it('returns true for numbers inside the range', () => { - expect(isNumberInRange(0, 0, 50)).toBe(true); - expect(isNumberInRange(50, 0, 50)).toBe(true); - expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true); - }); -}); - -describe('converts input to a number or null', () => { - it('returns null for invalid inputs', () => { - expect(toNumberOrNull(null)).toBeNull(); - // eslint-disable-next-line unicorn/no-useless-undefined - expect(toNumberOrNull(undefined)).toBeNull(); - expect(toNumberOrNull('')).toBeNull(); - expect(toNumberOrNull(Number.NaN)).toBeNull(); - }); - - it('returns a number for valid inputs', () => { - expect(toNumberOrNull(0)).toBeCloseTo(0); - expect(toNumberOrNull('0')).toBeCloseTo(0); - expect(toNumberOrNull('-123.45')).toBeCloseTo(-123.45); - }); -}); diff --git a/server/src/microservices/utils/numbers.ts b/server/src/microservices/utils/numbers.ts deleted file mode 100644 index cd6e81d2a..000000000 --- a/server/src/microservices/utils/numbers.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function isDecimalNumber(number_: number): boolean { - return !Number.isNaN(number_) && Number.isFinite(number_); -} - -/** - * Check if `num` is a valid number and is between `start` and `end` (inclusive) - */ -export function isNumberInRange(number_: number, start: number, end: number): boolean { - return isDecimalNumber(number_) && number_ >= start && number_ <= end; -} - -export function toNumberOrNull(input: number | string | null | undefined): number | null { - if (input === null || input === undefined) { - return null; - } - - const number_ = typeof input === 'string' ? Number.parseFloat(input) : input; - return isDecimalNumber(number_) ? number_ : null; -} diff --git a/server/src/immich/app.guard.ts b/server/src/middleware/auth.guard.ts similarity index 91% rename from server/src/immich/app.guard.ts rename to server/src/middleware/auth.guard.ts index bd07d107b..eaa47d013 100644 --- a/server/src/immich/app.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -1,5 +1,3 @@ -import { AuthDto, AuthService, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; import { CanActivate, ExecutionContext, @@ -11,6 +9,10 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; +import { IMMICH_API_KEY_NAME } from 'src/constants'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { ImmichLogger } from 'src/utils/logger'; import { UAParser } from 'ua-parser-js'; export enum Metadata { @@ -76,8 +78,8 @@ export interface AuthRequest extends Request { } @Injectable() -export class AppGuard implements CanActivate { - private logger = new ImmichLogger(AppGuard.name); +export class AuthGuard implements CanActivate { + private logger = new ImmichLogger(AuthGuard.name); constructor( private reflector: Reflector, diff --git a/server/src/immich/interceptors/error.interceptor.ts b/server/src/middleware/error.interceptor.ts similarity index 86% rename from server/src/immich/interceptors/error.interceptor.ts rename to server/src/middleware/error.interceptor.ts index 5fabdbe55..9e2273b97 100644 --- a/server/src/immich/interceptors/error.interceptor.ts +++ b/server/src/middleware/error.interceptor.ts @@ -1,4 +1,3 @@ -import { ImmichLogger } from '@app/infra/logger'; import { CallHandler, ExecutionContext, @@ -8,8 +7,8 @@ import { NestInterceptor, } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; -import { isConnectionAborted } from '../../domain'; -import { routeToErrorMessage } from '../app.utils'; +import { ImmichLogger } from 'src/utils/logger'; +import { isConnectionAborted, routeToErrorMessage } from 'src/utils/misc'; @Injectable() export class ErrorInterceptor implements NestInterceptor { diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts similarity index 95% rename from server/src/immich/interceptors/file-upload.interceptor.ts rename to server/src/middleware/file-upload.interceptor.ts index a698dc8a6..53acbefa8 100644 --- a/server/src/immich/interceptors/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -1,5 +1,3 @@ -import { AssetService, UploadFieldName, UploadFile } from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { PATH_METADATA } from '@nestjs/common/constants'; import { Reflector } from '@nestjs/core'; @@ -8,7 +6,10 @@ import { NextFunction, RequestHandler } from 'express'; import multer, { StorageEngine, diskStorage } from 'multer'; import { createHash, randomUUID } from 'node:crypto'; import { Observable } from 'rxjs'; -import { AuthRequest } from '../app.guard'; +import { UploadFieldName } from 'src/dtos/asset.dto'; +import { AuthRequest } from 'src/middleware/auth.guard'; +import { AssetService, UploadFile } from 'src/services/asset.service'; +import { ImmichLogger } from 'src/utils/logger'; export enum Route { ASSET = 'asset', diff --git a/server/src/infra/websocket.adapter.ts b/server/src/middleware/websocket.adapter.ts similarity index 100% rename from server/src/infra/websocket.adapter.ts rename to server/src/middleware/websocket.adapter.ts diff --git a/server/src/infra/migrations/1645130759468-CreateUserTable.ts b/server/src/migrations/1645130759468-CreateUserTable.ts similarity index 100% rename from server/src/infra/migrations/1645130759468-CreateUserTable.ts rename to server/src/migrations/1645130759468-CreateUserTable.ts diff --git a/server/src/infra/migrations/1645130777674-CreateDeviceInfoTable.ts b/server/src/migrations/1645130777674-CreateDeviceInfoTable.ts similarity index 100% rename from server/src/infra/migrations/1645130777674-CreateDeviceInfoTable.ts rename to server/src/migrations/1645130777674-CreateDeviceInfoTable.ts diff --git a/server/src/infra/migrations/1645130805273-CreateAssetsTable.ts b/server/src/migrations/1645130805273-CreateAssetsTable.ts similarity index 100% rename from server/src/infra/migrations/1645130805273-CreateAssetsTable.ts rename to server/src/migrations/1645130805273-CreateAssetsTable.ts diff --git a/server/src/infra/migrations/1645130817965-CreateExifTable.ts b/server/src/migrations/1645130817965-CreateExifTable.ts similarity index 100% rename from server/src/infra/migrations/1645130817965-CreateExifTable.ts rename to server/src/migrations/1645130817965-CreateExifTable.ts diff --git a/server/src/infra/migrations/1645130870184-CreateSmartInfoTable.ts b/server/src/migrations/1645130870184-CreateSmartInfoTable.ts similarity index 100% rename from server/src/infra/migrations/1645130870184-CreateSmartInfoTable.ts rename to server/src/migrations/1645130870184-CreateSmartInfoTable.ts diff --git a/server/src/infra/migrations/1646249209023-AddExifTextSearchColumn.ts b/server/src/migrations/1646249209023-AddExifTextSearchColumn.ts similarity index 100% rename from server/src/infra/migrations/1646249209023-AddExifTextSearchColumn.ts rename to server/src/migrations/1646249209023-AddExifTextSearchColumn.ts diff --git a/server/src/infra/migrations/1646249734844-CreateExifTextSearchIndex.ts b/server/src/migrations/1646249734844-CreateExifTextSearchIndex.ts similarity index 100% rename from server/src/infra/migrations/1646249734844-CreateExifTextSearchIndex.ts rename to server/src/migrations/1646249734844-CreateExifTextSearchIndex.ts diff --git a/server/src/infra/migrations/1646709533213-AddRegionCityToExIf.ts b/server/src/migrations/1646709533213-AddRegionCityToExIf.ts similarity index 100% rename from server/src/infra/migrations/1646709533213-AddRegionCityToExIf.ts rename to server/src/migrations/1646709533213-AddRegionCityToExIf.ts diff --git a/server/src/infra/migrations/1646710459852-AddLocationToExifTextSearch.ts b/server/src/migrations/1646710459852-AddLocationToExifTextSearch.ts similarity index 100% rename from server/src/infra/migrations/1646710459852-AddLocationToExifTextSearch.ts rename to server/src/migrations/1646710459852-AddLocationToExifTextSearch.ts diff --git a/server/src/infra/migrations/1648317474768-AddObjectColumnToSmartInfo.ts b/server/src/migrations/1648317474768-AddObjectColumnToSmartInfo.ts similarity index 100% rename from server/src/infra/migrations/1648317474768-AddObjectColumnToSmartInfo.ts rename to server/src/migrations/1648317474768-AddObjectColumnToSmartInfo.ts diff --git a/server/src/infra/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts b/server/src/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts similarity index 100% rename from server/src/infra/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts rename to server/src/migrations/1649643216111-CreateSharedAlbumAndRelatedTables.ts diff --git a/server/src/infra/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts b/server/src/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts similarity index 100% rename from server/src/infra/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts rename to server/src/migrations/1652633525943-UpdateUserTableWithAdminAndName.ts diff --git a/server/src/infra/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts b/server/src/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts similarity index 100% rename from server/src/infra/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts rename to server/src/migrations/1653214255670-UpdateAssetTableWithWebpPath.ts diff --git a/server/src/infra/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts b/server/src/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts similarity index 100% rename from server/src/infra/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts rename to server/src/migrations/1654299904583-UpdateAssetTableWithEncodeVideoPath.ts diff --git a/server/src/infra/migrations/1655401127251-RenameSharedAlbums.ts b/server/src/migrations/1655401127251-RenameSharedAlbums.ts similarity index 100% rename from server/src/infra/migrations/1655401127251-RenameSharedAlbums.ts rename to server/src/migrations/1655401127251-RenameSharedAlbums.ts diff --git a/server/src/infra/migrations/1656338626260-RenameIsFirstLoggedInColumn.ts b/server/src/migrations/1656338626260-RenameIsFirstLoggedInColumn.ts similarity index 100% rename from server/src/infra/migrations/1656338626260-RenameIsFirstLoggedInColumn.ts rename to server/src/migrations/1656338626260-RenameIsFirstLoggedInColumn.ts diff --git a/server/src/infra/migrations/1656888591977-RenameAssetAlbumIdSequence.ts b/server/src/migrations/1656888591977-RenameAssetAlbumIdSequence.ts similarity index 100% rename from server/src/infra/migrations/1656888591977-RenameAssetAlbumIdSequence.ts rename to server/src/migrations/1656888591977-RenameAssetAlbumIdSequence.ts diff --git a/server/src/infra/migrations/1656888918620-DropExifTextSearchableColumn.ts b/server/src/migrations/1656888918620-DropExifTextSearchableColumn.ts similarity index 100% rename from server/src/infra/migrations/1656888918620-DropExifTextSearchableColumn.ts rename to server/src/migrations/1656888918620-DropExifTextSearchableColumn.ts diff --git a/server/src/infra/migrations/1656889061566-MatchMigrationsWithTypeORMEntities.ts b/server/src/migrations/1656889061566-MatchMigrationsWithTypeORMEntities.ts similarity index 100% rename from server/src/infra/migrations/1656889061566-MatchMigrationsWithTypeORMEntities.ts rename to server/src/migrations/1656889061566-MatchMigrationsWithTypeORMEntities.ts diff --git a/server/src/infra/migrations/1658860470248-AddExifImageNameAsSearchableText.ts b/server/src/migrations/1658860470248-AddExifImageNameAsSearchableText.ts similarity index 100% rename from server/src/infra/migrations/1658860470248-AddExifImageNameAsSearchableText.ts rename to server/src/migrations/1658860470248-AddExifImageNameAsSearchableText.ts diff --git a/server/src/infra/migrations/1661011331242-AddCaption.ts b/server/src/migrations/1661011331242-AddCaption.ts similarity index 100% rename from server/src/infra/migrations/1661011331242-AddCaption.ts rename to server/src/migrations/1661011331242-AddCaption.ts diff --git a/server/src/infra/migrations/1661528919411-ChangeExifFileSizeInByteToBigInt.ts b/server/src/migrations/1661528919411-ChangeExifFileSizeInByteToBigInt.ts similarity index 100% rename from server/src/infra/migrations/1661528919411-ChangeExifFileSizeInByteToBigInt.ts rename to server/src/migrations/1661528919411-ChangeExifFileSizeInByteToBigInt.ts diff --git a/server/src/infra/migrations/1661881837496-AddAssetChecksum.ts b/server/src/migrations/1661881837496-AddAssetChecksum.ts similarity index 100% rename from server/src/infra/migrations/1661881837496-AddAssetChecksum.ts rename to server/src/migrations/1661881837496-AddAssetChecksum.ts diff --git a/server/src/infra/migrations/1661971370662-UpdateAssetTableWithNewUniqueConstraint.ts b/server/src/migrations/1661971370662-UpdateAssetTableWithNewUniqueConstraint.ts similarity index 100% rename from server/src/infra/migrations/1661971370662-UpdateAssetTableWithNewUniqueConstraint.ts rename to server/src/migrations/1661971370662-UpdateAssetTableWithNewUniqueConstraint.ts diff --git a/server/src/infra/migrations/1662427365521-FixTimestampDataTypeInAssetTable.ts b/server/src/migrations/1662427365521-FixTimestampDataTypeInAssetTable.ts similarity index 100% rename from server/src/infra/migrations/1662427365521-FixTimestampDataTypeInAssetTable.ts rename to server/src/migrations/1662427365521-FixTimestampDataTypeInAssetTable.ts diff --git a/server/src/infra/migrations/1665540663419-CreateSystemConfigTable.ts b/server/src/migrations/1665540663419-CreateSystemConfigTable.ts similarity index 100% rename from server/src/infra/migrations/1665540663419-CreateSystemConfigTable.ts rename to server/src/migrations/1665540663419-CreateSystemConfigTable.ts diff --git a/server/src/infra/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts b/server/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts similarity index 100% rename from server/src/infra/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts rename to server/src/migrations/1667762360744-AddingDeletedAtColumnInUserEntity.ts diff --git a/server/src/infra/migrations/1668383120461-AddLivePhotosRelatedColumnToAssetTable.ts b/server/src/migrations/1668383120461-AddLivePhotosRelatedColumnToAssetTable.ts similarity index 100% rename from server/src/infra/migrations/1668383120461-AddLivePhotosRelatedColumnToAssetTable.ts rename to server/src/migrations/1668383120461-AddLivePhotosRelatedColumnToAssetTable.ts diff --git a/server/src/infra/migrations/1668835311083-UpdateUserTableForOIDC.ts b/server/src/migrations/1668835311083-UpdateUserTableForOIDC.ts similarity index 100% rename from server/src/infra/migrations/1668835311083-UpdateUserTableForOIDC.ts rename to server/src/migrations/1668835311083-UpdateUserTableForOIDC.ts diff --git a/server/src/infra/migrations/1670104716264-OAuthId.ts b/server/src/migrations/1670104716264-OAuthId.ts similarity index 100% rename from server/src/infra/migrations/1670104716264-OAuthId.ts rename to server/src/migrations/1670104716264-OAuthId.ts diff --git a/server/src/infra/migrations/1670257571385-CreateTagsTable.ts b/server/src/migrations/1670257571385-CreateTagsTable.ts similarity index 100% rename from server/src/infra/migrations/1670257571385-CreateTagsTable.ts rename to server/src/migrations/1670257571385-CreateTagsTable.ts diff --git a/server/src/infra/migrations/1670607437008-TruncateOldConfigItems.ts b/server/src/migrations/1670607437008-TruncateOldConfigItems.ts similarity index 100% rename from server/src/infra/migrations/1670607437008-TruncateOldConfigItems.ts rename to server/src/migrations/1670607437008-TruncateOldConfigItems.ts diff --git a/server/src/infra/migrations/1670633210032-AddUserEmailUniqueConstraint.ts b/server/src/migrations/1670633210032-AddUserEmailUniqueConstraint.ts similarity index 100% rename from server/src/infra/migrations/1670633210032-AddUserEmailUniqueConstraint.ts rename to server/src/migrations/1670633210032-AddUserEmailUniqueConstraint.ts diff --git a/server/src/infra/migrations/1672109862870-DropSaltColumn.ts b/server/src/migrations/1672109862870-DropSaltColumn.ts similarity index 100% rename from server/src/infra/migrations/1672109862870-DropSaltColumn.ts rename to server/src/migrations/1672109862870-DropSaltColumn.ts diff --git a/server/src/infra/migrations/1672502270115-AddAPIKeys.ts b/server/src/migrations/1672502270115-AddAPIKeys.ts similarity index 100% rename from server/src/infra/migrations/1672502270115-AddAPIKeys.ts rename to server/src/migrations/1672502270115-AddAPIKeys.ts diff --git a/server/src/infra/migrations/1673150490490-AddSharedLinkTable.ts b/server/src/migrations/1673150490490-AddSharedLinkTable.ts similarity index 100% rename from server/src/infra/migrations/1673150490490-AddSharedLinkTable.ts rename to server/src/migrations/1673150490490-AddSharedLinkTable.ts diff --git a/server/src/infra/migrations/1673907194740-AddMorePermissionToSharedLink.ts b/server/src/migrations/1673907194740-AddMorePermissionToSharedLink.ts similarity index 100% rename from server/src/infra/migrations/1673907194740-AddMorePermissionToSharedLink.ts rename to server/src/migrations/1673907194740-AddMorePermissionToSharedLink.ts diff --git a/server/src/infra/migrations/1674263302005-RemoveVideoCodecConfigOption.ts b/server/src/migrations/1674263302005-RemoveVideoCodecConfigOption.ts similarity index 100% rename from server/src/infra/migrations/1674263302005-RemoveVideoCodecConfigOption.ts rename to server/src/migrations/1674263302005-RemoveVideoCodecConfigOption.ts diff --git a/server/src/infra/migrations/1674342044239-CreateUserTokenEntity.ts b/server/src/migrations/1674342044239-CreateUserTokenEntity.ts similarity index 100% rename from server/src/infra/migrations/1674342044239-CreateUserTokenEntity.ts rename to server/src/migrations/1674342044239-CreateUserTokenEntity.ts diff --git a/server/src/infra/migrations/1674757936889-AlterExifExposureTimeToString.ts b/server/src/migrations/1674757936889-AlterExifExposureTimeToString.ts similarity index 100% rename from server/src/infra/migrations/1674757936889-AlterExifExposureTimeToString.ts rename to server/src/migrations/1674757936889-AlterExifExposureTimeToString.ts diff --git a/server/src/infra/migrations/1674774248319-TruncateAPIKeys.ts b/server/src/migrations/1674774248319-TruncateAPIKeys.ts similarity index 100% rename from server/src/infra/migrations/1674774248319-TruncateAPIKeys.ts rename to server/src/migrations/1674774248319-TruncateAPIKeys.ts diff --git a/server/src/infra/migrations/1674939383309-AddSharedLinkUserForeignKeyConstraint.ts b/server/src/migrations/1674939383309-AddSharedLinkUserForeignKeyConstraint.ts similarity index 100% rename from server/src/infra/migrations/1674939383309-AddSharedLinkUserForeignKeyConstraint.ts rename to server/src/migrations/1674939383309-AddSharedLinkUserForeignKeyConstraint.ts diff --git a/server/src/infra/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts b/server/src/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts similarity index 100% rename from server/src/infra/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts rename to server/src/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts diff --git a/server/src/infra/migrations/1675701909594-AddAlbumUserForeignKeyConstraint.ts b/server/src/migrations/1675701909594-AddAlbumUserForeignKeyConstraint.ts similarity index 100% rename from server/src/infra/migrations/1675701909594-AddAlbumUserForeignKeyConstraint.ts rename to server/src/migrations/1675701909594-AddAlbumUserForeignKeyConstraint.ts diff --git a/server/src/infra/migrations/1675808874445-APIKeyUUIDPrimaryKey.ts b/server/src/migrations/1675808874445-APIKeyUUIDPrimaryKey.ts similarity index 100% rename from server/src/infra/migrations/1675808874445-APIKeyUUIDPrimaryKey.ts rename to server/src/migrations/1675808874445-APIKeyUUIDPrimaryKey.ts diff --git a/server/src/infra/migrations/1675812532822-FixAlbumEntityTypeORM.ts b/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts similarity index 100% rename from server/src/infra/migrations/1675812532822-FixAlbumEntityTypeORM.ts rename to server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts diff --git a/server/src/infra/migrations/1676437878377-AppleContentIdentifier.ts b/server/src/migrations/1676437878377-AppleContentIdentifier.ts similarity index 100% rename from server/src/infra/migrations/1676437878377-AppleContentIdentifier.ts rename to server/src/migrations/1676437878377-AppleContentIdentifier.ts diff --git a/server/src/infra/migrations/1676680127415-FixAssetRelations.ts b/server/src/migrations/1676680127415-FixAssetRelations.ts similarity index 100% rename from server/src/infra/migrations/1676680127415-FixAssetRelations.ts rename to server/src/migrations/1676680127415-FixAssetRelations.ts diff --git a/server/src/infra/migrations/1676721296440-AssetCreatedAtField.ts b/server/src/migrations/1676721296440-AssetCreatedAtField.ts similarity index 100% rename from server/src/infra/migrations/1676721296440-AssetCreatedAtField.ts rename to server/src/migrations/1676721296440-AssetCreatedAtField.ts diff --git a/server/src/infra/migrations/1676848629119-ExifEntityDefinitionFixes.ts b/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts similarity index 100% rename from server/src/infra/migrations/1676848629119-ExifEntityDefinitionFixes.ts rename to server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts diff --git a/server/src/infra/migrations/1676848694786-SharedLinkEntityDefinitionFixes.ts b/server/src/migrations/1676848694786-SharedLinkEntityDefinitionFixes.ts similarity index 100% rename from server/src/infra/migrations/1676848694786-SharedLinkEntityDefinitionFixes.ts rename to server/src/migrations/1676848694786-SharedLinkEntityDefinitionFixes.ts diff --git a/server/src/infra/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts b/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts similarity index 100% rename from server/src/infra/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts rename to server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts diff --git a/server/src/infra/migrations/1677497925328-AddExifTimeZone.ts b/server/src/migrations/1677497925328-AddExifTimeZone.ts similarity index 100% rename from server/src/infra/migrations/1677497925328-AddExifTimeZone.ts rename to server/src/migrations/1677497925328-AddExifTimeZone.ts diff --git a/server/src/infra/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts b/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts similarity index 100% rename from server/src/infra/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts rename to server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts diff --git a/server/src/infra/migrations/1677613712565-AlbumThumbnailRelation.ts b/server/src/migrations/1677613712565-AlbumThumbnailRelation.ts similarity index 100% rename from server/src/infra/migrations/1677613712565-AlbumThumbnailRelation.ts rename to server/src/migrations/1677613712565-AlbumThumbnailRelation.ts diff --git a/server/src/infra/migrations/1677971458822-AddCLIPEncodeDataColumn.ts b/server/src/migrations/1677971458822-AddCLIPEncodeDataColumn.ts similarity index 100% rename from server/src/infra/migrations/1677971458822-AddCLIPEncodeDataColumn.ts rename to server/src/migrations/1677971458822-AddCLIPEncodeDataColumn.ts diff --git a/server/src/infra/migrations/1679751316282-UpdateTranscodeOption.ts b/server/src/migrations/1679751316282-UpdateTranscodeOption.ts similarity index 100% rename from server/src/infra/migrations/1679751316282-UpdateTranscodeOption.ts rename to server/src/migrations/1679751316282-UpdateTranscodeOption.ts diff --git a/server/src/infra/migrations/1679901204458-ClipEmbeddingFloat4.ts b/server/src/migrations/1679901204458-ClipEmbeddingFloat4.ts similarity index 100% rename from server/src/infra/migrations/1679901204458-ClipEmbeddingFloat4.ts rename to server/src/migrations/1679901204458-ClipEmbeddingFloat4.ts diff --git a/server/src/infra/migrations/1680632845740-AddIsArchivedColumn.ts b/server/src/migrations/1680632845740-AddIsArchivedColumn.ts similarity index 100% rename from server/src/infra/migrations/1680632845740-AddIsArchivedColumn.ts rename to server/src/migrations/1680632845740-AddIsArchivedColumn.ts diff --git a/server/src/infra/migrations/1680694465853-RemoveRedundantConstraints.ts b/server/src/migrations/1680694465853-RemoveRedundantConstraints.ts similarity index 100% rename from server/src/infra/migrations/1680694465853-RemoveRedundantConstraints.ts rename to server/src/migrations/1680694465853-RemoveRedundantConstraints.ts diff --git a/server/src/infra/migrations/1681144628393-AddOriginalFileNameToAssetTable.ts b/server/src/migrations/1681144628393-AddOriginalFileNameToAssetTable.ts similarity index 100% rename from server/src/infra/migrations/1681144628393-AddOriginalFileNameToAssetTable.ts rename to server/src/migrations/1681144628393-AddOriginalFileNameToAssetTable.ts diff --git a/server/src/infra/migrations/1681159594469-RemoveImageNameFromEXIFTable.ts b/server/src/migrations/1681159594469-RemoveImageNameFromEXIFTable.ts similarity index 100% rename from server/src/infra/migrations/1681159594469-RemoveImageNameFromEXIFTable.ts rename to server/src/migrations/1681159594469-RemoveImageNameFromEXIFTable.ts diff --git a/server/src/infra/migrations/1682371561743-FixNullableRelations.ts b/server/src/migrations/1682371561743-FixNullableRelations.ts similarity index 100% rename from server/src/infra/migrations/1682371561743-FixNullableRelations.ts rename to server/src/migrations/1682371561743-FixNullableRelations.ts diff --git a/server/src/infra/migrations/1682371791038-AddDeviceInfoToUserToken.ts b/server/src/migrations/1682371791038-AddDeviceInfoToUserToken.ts similarity index 100% rename from server/src/infra/migrations/1682371791038-AddDeviceInfoToUserToken.ts rename to server/src/migrations/1682371791038-AddDeviceInfoToUserToken.ts diff --git a/server/src/infra/migrations/1682710252424-DropDeviceInfoTable.ts b/server/src/migrations/1682710252424-DropDeviceInfoTable.ts similarity index 100% rename from server/src/infra/migrations/1682710252424-DropDeviceInfoTable.ts rename to server/src/migrations/1682710252424-DropDeviceInfoTable.ts diff --git a/server/src/infra/migrations/1683808254676-AddPartnersTable.ts b/server/src/migrations/1683808254676-AddPartnersTable.ts similarity index 100% rename from server/src/infra/migrations/1683808254676-AddPartnersTable.ts rename to server/src/migrations/1683808254676-AddPartnersTable.ts diff --git a/server/src/infra/migrations/1684255168091-AddFacialTables.ts b/server/src/migrations/1684255168091-AddFacialTables.ts similarity index 100% rename from server/src/infra/migrations/1684255168091-AddFacialTables.ts rename to server/src/migrations/1684255168091-AddFacialTables.ts diff --git a/server/src/infra/migrations/1684273840676-AddSidecarFile.ts b/server/src/migrations/1684273840676-AddSidecarFile.ts similarity index 100% rename from server/src/infra/migrations/1684273840676-AddSidecarFile.ts rename to server/src/migrations/1684273840676-AddSidecarFile.ts diff --git a/server/src/infra/migrations/1684328185099-RequireChecksumNotNull.ts b/server/src/migrations/1684328185099-RequireChecksumNotNull.ts similarity index 100% rename from server/src/infra/migrations/1684328185099-RequireChecksumNotNull.ts rename to server/src/migrations/1684328185099-RequireChecksumNotNull.ts diff --git a/server/src/infra/migrations/1684410565398-AddStorageLabel.ts b/server/src/migrations/1684410565398-AddStorageLabel.ts similarity index 100% rename from server/src/infra/migrations/1684410565398-AddStorageLabel.ts rename to server/src/migrations/1684410565398-AddStorageLabel.ts diff --git a/server/src/infra/migrations/1684867360825-AddUserTokenAndAPIKeyCascades.ts b/server/src/migrations/1684867360825-AddUserTokenAndAPIKeyCascades.ts similarity index 100% rename from server/src/infra/migrations/1684867360825-AddUserTokenAndAPIKeyCascades.ts rename to server/src/migrations/1684867360825-AddUserTokenAndAPIKeyCascades.ts diff --git a/server/src/infra/migrations/1685044328272-AddSharedLinkCascade.ts b/server/src/migrations/1685044328272-AddSharedLinkCascade.ts similarity index 100% rename from server/src/infra/migrations/1685044328272-AddSharedLinkCascade.ts rename to server/src/migrations/1685044328272-AddSharedLinkCascade.ts diff --git a/server/src/infra/migrations/1685370430343-UserDatesTimestamptz.ts b/server/src/migrations/1685370430343-UserDatesTimestamptz.ts similarity index 100% rename from server/src/infra/migrations/1685370430343-UserDatesTimestamptz.ts rename to server/src/migrations/1685370430343-UserDatesTimestamptz.ts diff --git a/server/src/infra/migrations/1685731372040-RemoveInvalidCoordinates.ts b/server/src/migrations/1685731372040-RemoveInvalidCoordinates.ts similarity index 100% rename from server/src/infra/migrations/1685731372040-RemoveInvalidCoordinates.ts rename to server/src/migrations/1685731372040-RemoveInvalidCoordinates.ts diff --git a/server/src/infra/migrations/1686584273471-ImportAsset.ts b/server/src/migrations/1686584273471-ImportAsset.ts similarity index 100% rename from server/src/infra/migrations/1686584273471-ImportAsset.ts rename to server/src/migrations/1686584273471-ImportAsset.ts diff --git a/server/src/infra/migrations/1686762895180-AddThumbhashColumn.ts b/server/src/migrations/1686762895180-AddThumbhashColumn.ts similarity index 100% rename from server/src/infra/migrations/1686762895180-AddThumbhashColumn.ts rename to server/src/migrations/1686762895180-AddThumbhashColumn.ts diff --git a/server/src/infra/migrations/1688241394489-AddDetectFaceResultInfo.ts b/server/src/migrations/1688241394489-AddDetectFaceResultInfo.ts similarity index 100% rename from server/src/infra/migrations/1688241394489-AddDetectFaceResultInfo.ts rename to server/src/migrations/1688241394489-AddDetectFaceResultInfo.ts diff --git a/server/src/infra/migrations/1688392120838-AddLibraryTable.ts b/server/src/migrations/1688392120838-AddLibraryTable.ts similarity index 100% rename from server/src/infra/migrations/1688392120838-AddLibraryTable.ts rename to server/src/migrations/1688392120838-AddLibraryTable.ts diff --git a/server/src/infra/migrations/1689001889950-DropMimeTypeColumn.ts b/server/src/migrations/1689001889950-DropMimeTypeColumn.ts similarity index 100% rename from server/src/infra/migrations/1689001889950-DropMimeTypeColumn.ts rename to server/src/migrations/1689001889950-DropMimeTypeColumn.ts diff --git a/server/src/infra/migrations/1689281196844-AddHiddenFaces.ts b/server/src/migrations/1689281196844-AddHiddenFaces.ts similarity index 100% rename from server/src/infra/migrations/1689281196844-AddHiddenFaces.ts rename to server/src/migrations/1689281196844-AddHiddenFaces.ts diff --git a/server/src/infra/migrations/1690469489288-Panoramas.ts b/server/src/migrations/1690469489288-Panoramas.ts similarity index 100% rename from server/src/infra/migrations/1690469489288-Panoramas.ts rename to server/src/migrations/1690469489288-Panoramas.ts diff --git a/server/src/infra/migrations/1691209138541-AddAlbumDescription.ts b/server/src/migrations/1691209138541-AddAlbumDescription.ts similarity index 100% rename from server/src/infra/migrations/1691209138541-AddAlbumDescription.ts rename to server/src/migrations/1691209138541-AddAlbumDescription.ts diff --git a/server/src/infra/migrations/1691600216749-UserMemoryPreference.ts b/server/src/migrations/1691600216749-UserMemoryPreference.ts similarity index 100% rename from server/src/infra/migrations/1691600216749-UserMemoryPreference.ts rename to server/src/migrations/1691600216749-UserMemoryPreference.ts diff --git a/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts b/server/src/migrations/1692057328660-fixGPSNullIsland.ts similarity index 100% rename from server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts rename to server/src/migrations/1692057328660-fixGPSNullIsland.ts diff --git a/server/src/infra/migrations/1692112147855-AddPersonBirthDate.ts b/server/src/migrations/1692112147855-AddPersonBirthDate.ts similarity index 100% rename from server/src/infra/migrations/1692112147855-AddPersonBirthDate.ts rename to server/src/migrations/1692112147855-AddPersonBirthDate.ts diff --git a/server/src/infra/migrations/1692804658140-AddAuditTable.ts b/server/src/migrations/1692804658140-AddAuditTable.ts similarity index 100% rename from server/src/infra/migrations/1692804658140-AddAuditTable.ts rename to server/src/migrations/1692804658140-AddAuditTable.ts diff --git a/server/src/infra/migrations/1693236627291-RenameMLEnableFlags.ts b/server/src/migrations/1693236627291-RenameMLEnableFlags.ts similarity index 100% rename from server/src/infra/migrations/1693236627291-RenameMLEnableFlags.ts rename to server/src/migrations/1693236627291-RenameMLEnableFlags.ts diff --git a/server/src/infra/migrations/1693833336881-AddPersonFaceAssetId.ts b/server/src/migrations/1693833336881-AddPersonFaceAssetId.ts similarity index 100% rename from server/src/infra/migrations/1693833336881-AddPersonFaceAssetId.ts rename to server/src/migrations/1693833336881-AddPersonFaceAssetId.ts diff --git a/server/src/infra/migrations/1694204416744-AddAssetDeletedAtColumn.ts b/server/src/migrations/1694204416744-AddAssetDeletedAtColumn.ts similarity index 100% rename from server/src/infra/migrations/1694204416744-AddAssetDeletedAtColumn.ts rename to server/src/migrations/1694204416744-AddAssetDeletedAtColumn.ts diff --git a/server/src/infra/migrations/1694525143117-AddLocalDateTime.ts b/server/src/migrations/1694525143117-AddLocalDateTime.ts similarity index 100% rename from server/src/infra/migrations/1694525143117-AddLocalDateTime.ts rename to server/src/migrations/1694525143117-AddLocalDateTime.ts diff --git a/server/src/infra/migrations/1694638413248-AddDeletedAtToAlbums.ts b/server/src/migrations/1694638413248-AddDeletedAtToAlbums.ts similarity index 100% rename from server/src/infra/migrations/1694638413248-AddDeletedAtToAlbums.ts rename to server/src/migrations/1694638413248-AddDeletedAtToAlbums.ts diff --git a/server/src/infra/migrations/1694750975773-AddExifColorSpace.ts b/server/src/migrations/1694750975773-AddExifColorSpace.ts similarity index 100% rename from server/src/infra/migrations/1694750975773-AddExifColorSpace.ts rename to server/src/migrations/1694750975773-AddExifColorSpace.ts diff --git a/server/src/infra/migrations/1694758412194-UpdateOpusCodecToLibopus.ts b/server/src/migrations/1694758412194-UpdateOpusCodecToLibopus.ts similarity index 100% rename from server/src/infra/migrations/1694758412194-UpdateOpusCodecToLibopus.ts rename to server/src/migrations/1694758412194-UpdateOpusCodecToLibopus.ts diff --git a/server/src/infra/migrations/1695354433573-AddStackParentIdToAssets.ts b/server/src/migrations/1695354433573-AddStackParentIdToAssets.ts similarity index 100% rename from server/src/infra/migrations/1695354433573-AddStackParentIdToAssets.ts rename to server/src/migrations/1695354433573-AddStackParentIdToAssets.ts diff --git a/server/src/infra/migrations/1695660378655-RemoveInvalidCoordinates.ts b/server/src/migrations/1695660378655-RemoveInvalidCoordinates.ts similarity index 100% rename from server/src/infra/migrations/1695660378655-RemoveInvalidCoordinates.ts rename to server/src/migrations/1695660378655-RemoveInvalidCoordinates.ts diff --git a/server/src/infra/migrations/1696888644031-AddOriginalPathIndex.ts b/server/src/migrations/1696888644031-AddOriginalPathIndex.ts similarity index 100% rename from server/src/infra/migrations/1696888644031-AddOriginalPathIndex.ts rename to server/src/migrations/1696888644031-AddOriginalPathIndex.ts diff --git a/server/src/infra/migrations/1696968880063-AddMoveTable.ts b/server/src/migrations/1696968880063-AddMoveTable.ts similarity index 100% rename from server/src/infra/migrations/1696968880063-AddMoveTable.ts rename to server/src/migrations/1696968880063-AddMoveTable.ts diff --git a/server/src/infra/migrations/1697272818851-UnassignFace.ts b/server/src/migrations/1697272818851-UnassignFace.ts similarity index 100% rename from server/src/infra/migrations/1697272818851-UnassignFace.ts rename to server/src/migrations/1697272818851-UnassignFace.ts diff --git a/server/src/infra/migrations/1698290827089-AddPasswordToSharedLinks.ts b/server/src/migrations/1698290827089-AddPasswordToSharedLinks.ts similarity index 100% rename from server/src/infra/migrations/1698290827089-AddPasswordToSharedLinks.ts rename to server/src/migrations/1698290827089-AddPasswordToSharedLinks.ts diff --git a/server/src/infra/migrations/1698693294632-AddActivity.ts b/server/src/migrations/1698693294632-AddActivity.ts similarity index 100% rename from server/src/infra/migrations/1698693294632-AddActivity.ts rename to server/src/migrations/1698693294632-AddActivity.ts diff --git a/server/src/infra/migrations/1699268680508-DisableActivity.ts b/server/src/migrations/1699268680508-DisableActivity.ts similarity index 100% rename from server/src/infra/migrations/1699268680508-DisableActivity.ts rename to server/src/migrations/1699268680508-DisableActivity.ts diff --git a/server/src/infra/migrations/1699322864544-UserNameConsolidation.ts b/server/src/migrations/1699322864544-UserNameConsolidation.ts similarity index 100% rename from server/src/infra/migrations/1699322864544-UserNameConsolidation.ts rename to server/src/migrations/1699322864544-UserNameConsolidation.ts diff --git a/server/src/infra/migrations/1699345863886-AddJobStatus.ts b/server/src/migrations/1699345863886-AddJobStatus.ts similarity index 100% rename from server/src/infra/migrations/1699345863886-AddJobStatus.ts rename to server/src/migrations/1699345863886-AddJobStatus.ts diff --git a/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts b/server/src/migrations/1699562570201-AdddInTimelineToPartnersTable.ts similarity index 100% rename from server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts rename to server/src/migrations/1699562570201-AdddInTimelineToPartnersTable.ts diff --git a/server/src/infra/migrations/1699727044012-EditFaceAssetForeignKey.ts b/server/src/migrations/1699727044012-EditFaceAssetForeignKey.ts similarity index 100% rename from server/src/infra/migrations/1699727044012-EditFaceAssetForeignKey.ts rename to server/src/migrations/1699727044012-EditFaceAssetForeignKey.ts diff --git a/server/src/infra/migrations/1699889987493-AddAvatarColor.ts b/server/src/migrations/1699889987493-AddAvatarColor.ts similarity index 100% rename from server/src/infra/migrations/1699889987493-AddAvatarColor.ts rename to server/src/migrations/1699889987493-AddAvatarColor.ts diff --git a/server/src/infra/migrations/1700345818045-SystemMetadata.ts b/server/src/migrations/1700345818045-SystemMetadata.ts similarity index 100% rename from server/src/infra/migrations/1700345818045-SystemMetadata.ts rename to server/src/migrations/1700345818045-SystemMetadata.ts diff --git a/server/src/infra/migrations/1700362016675-Geodata.ts b/server/src/migrations/1700362016675-Geodata.ts similarity index 100% rename from server/src/infra/migrations/1700362016675-Geodata.ts rename to server/src/migrations/1700362016675-Geodata.ts diff --git a/server/src/infra/migrations/1700713871511-UsePgVectors.ts b/server/src/migrations/1700713871511-UsePgVectors.ts similarity index 87% rename from server/src/infra/migrations/1700713871511-UsePgVectors.ts rename to server/src/migrations/1700713871511-UsePgVectors.ts index 008d5eadc..75c85e3e0 100644 --- a/server/src/infra/migrations/1700713871511-UsePgVectors.ts +++ b/server/src/migrations/1700713871511-UsePgVectors.ts @@ -1,6 +1,6 @@ -import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; +import { vectorExt } from 'src/database.config'; +import { getCLIPModelInfo } from 'src/utils/misc'; import { MigrationInterface, QueryRunner } from 'typeorm'; -import { vectorExt } from '@app/infra/database.config'; export class UsePgVectors1700713871511 implements MigrationInterface { name = 'UsePgVectors1700713871511'; @@ -14,12 +14,14 @@ export class UsePgVectors1700713871511 implements MigrationInterface { LIMIT 1`); const faceDimSize = faceDimQuery?.[0]?.['dimsize'] ?? 512; - const clipModelNameQuery = await queryRunner.query(`SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`); + const clipModelNameQuery = await queryRunner.query( + `SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`, + ); const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai'; const clipDimSize = getCLIPModelInfo(clipModelName.replaceAll('"', '')).dimSize; await queryRunner.query(` - ALTER TABLE asset_faces + ALTER TABLE asset_faces ALTER COLUMN embedding SET NOT NULL, ALTER COLUMN embedding TYPE vector(${faceDimSize})`); @@ -37,7 +39,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface { AND array_position(si."clipEmbedding", NULL) IS NULL`); await queryRunner.query(`ALTER TABLE smart_info DROP COLUMN IF EXISTS "clipEmbedding"`); - } + } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE asset_faces ALTER COLUMN embedding TYPE real array`); diff --git a/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts similarity index 85% rename from server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts rename to server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts index c3716cc19..908ebdb8f 100644 --- a/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts +++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts @@ -1,6 +1,6 @@ +import { vectorExt } from 'src/database.config'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { MigrationInterface, QueryRunner } from 'typeorm'; -import { vectorExt } from '../database.config'; -import { DatabaseExtension } from '@app/domain/repositories/database.repository'; export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface { name = 'AddCLIPEmbeddingIndex1700713994428'; diff --git a/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts similarity index 85% rename from server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts rename to server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts index 066303530..75bebfa8e 100644 --- a/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts +++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts @@ -1,6 +1,6 @@ +import { vectorExt } from 'src/database.config'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { MigrationInterface, QueryRunner } from 'typeorm'; -import { vectorExt } from '../database.config'; -import { DatabaseExtension } from '@app/domain/repositories/database.repository'; export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface { name = 'AddFaceEmbeddingIndex1700714033632'; diff --git a/server/src/infra/migrations/1700714072055-AddSmartInfoTagsIndex.ts b/server/src/migrations/1700714072055-AddSmartInfoTagsIndex.ts similarity index 100% rename from server/src/infra/migrations/1700714072055-AddSmartInfoTagsIndex.ts rename to server/src/migrations/1700714072055-AddSmartInfoTagsIndex.ts diff --git a/server/src/infra/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts b/server/src/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts similarity index 100% rename from server/src/infra/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts rename to server/src/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts diff --git a/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts similarity index 100% rename from server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts rename to server/src/migrations/1700752078178-AddAssetFaceIndicies.ts diff --git a/server/src/infra/migrations/1701665867595-AddExifCityIndex.ts b/server/src/migrations/1701665867595-AddExifCityIndex.ts similarity index 100% rename from server/src/infra/migrations/1701665867595-AddExifCityIndex.ts rename to server/src/migrations/1701665867595-AddExifCityIndex.ts diff --git a/server/src/infra/migrations/1702084989965-AddWebSocketAttachmentTable.ts b/server/src/migrations/1702084989965-AddWebSocketAttachmentTable.ts similarity index 100% rename from server/src/infra/migrations/1702084989965-AddWebSocketAttachmentTable.ts rename to server/src/migrations/1702084989965-AddWebSocketAttachmentTable.ts diff --git a/server/src/infra/migrations/1702257380990-DropNullIslandLatLong.ts b/server/src/migrations/1702257380990-DropNullIslandLatLong.ts similarity index 100% rename from server/src/infra/migrations/1702257380990-DropNullIslandLatLong.ts rename to server/src/migrations/1702257380990-DropNullIslandLatLong.ts diff --git a/server/src/infra/migrations/1702938928766-NullifyFutureBirthDatesAndAddCheckConstraint.ts b/server/src/migrations/1702938928766-NullifyFutureBirthDatesAndAddCheckConstraint.ts similarity index 100% rename from server/src/infra/migrations/1702938928766-NullifyFutureBirthDatesAndAddCheckConstraint.ts rename to server/src/migrations/1702938928766-NullifyFutureBirthDatesAndAddCheckConstraint.ts diff --git a/server/src/infra/migrations/1702942303661-FixRemovedAssetsSharedLink.ts b/server/src/migrations/1702942303661-FixRemovedAssetsSharedLink.ts similarity index 100% rename from server/src/infra/migrations/1702942303661-FixRemovedAssetsSharedLink.ts rename to server/src/migrations/1702942303661-FixRemovedAssetsSharedLink.ts diff --git a/server/src/infra/migrations/1703035138085-AddAutoStackId.ts b/server/src/migrations/1703035138085-AddAutoStackId.ts similarity index 100% rename from server/src/infra/migrations/1703035138085-AddAutoStackId.ts rename to server/src/migrations/1703035138085-AddAutoStackId.ts diff --git a/server/src/infra/migrations/1703288449127-DefaultStorageTemplateOnForExistingInstallations.ts b/server/src/migrations/1703288449127-DefaultStorageTemplateOnForExistingInstallations.ts similarity index 100% rename from server/src/infra/migrations/1703288449127-DefaultStorageTemplateOnForExistingInstallations.ts rename to server/src/migrations/1703288449127-DefaultStorageTemplateOnForExistingInstallations.ts diff --git a/server/src/infra/migrations/1704382918223-AddQuotaColumnsToUser.ts b/server/src/migrations/1704382918223-AddQuotaColumnsToUser.ts similarity index 100% rename from server/src/infra/migrations/1704382918223-AddQuotaColumnsToUser.ts rename to server/src/migrations/1704382918223-AddQuotaColumnsToUser.ts diff --git a/server/src/infra/migrations/1704571051932-DefaultOnboardingForExistingInstallations.ts b/server/src/migrations/1704571051932-DefaultOnboardingForExistingInstallations.ts similarity index 100% rename from server/src/infra/migrations/1704571051932-DefaultOnboardingForExistingInstallations.ts rename to server/src/migrations/1704571051932-DefaultOnboardingForExistingInstallations.ts diff --git a/server/src/infra/migrations/1704943345360-SetAssetFaceNullOnPersonDelete.ts b/server/src/migrations/1704943345360-SetAssetFaceNullOnPersonDelete.ts similarity index 100% rename from server/src/infra/migrations/1704943345360-SetAssetFaceNullOnPersonDelete.ts rename to server/src/migrations/1704943345360-SetAssetFaceNullOnPersonDelete.ts diff --git a/server/src/infra/migrations/1705094221536-AddMetadataExtractedAt.ts b/server/src/migrations/1705094221536-AddMetadataExtractedAt.ts similarity index 100% rename from server/src/infra/migrations/1705094221536-AddMetadataExtractedAt.ts rename to server/src/migrations/1705094221536-AddMetadataExtractedAt.ts diff --git a/server/src/infra/migrations/1705306747072-AddOriginalFileNameIndex.ts b/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts similarity index 100% rename from server/src/infra/migrations/1705306747072-AddOriginalFileNameIndex.ts rename to server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts diff --git a/server/src/infra/migrations/1705363967169-CreateAssetStackTable.ts b/server/src/migrations/1705363967169-CreateAssetStackTable.ts similarity index 100% rename from server/src/infra/migrations/1705363967169-CreateAssetStackTable.ts rename to server/src/migrations/1705363967169-CreateAssetStackTable.ts diff --git a/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts b/server/src/migrations/1707000751533-AddVectorsToSearchPath.ts similarity index 100% rename from server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts rename to server/src/migrations/1707000751533-AddVectorsToSearchPath.ts diff --git a/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts b/server/src/migrations/1708059341865-GeodataLocationSearch.ts similarity index 100% rename from server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts rename to server/src/migrations/1708059341865-GeodataLocationSearch.ts diff --git a/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts b/server/src/migrations/1708116312820-GeonamesEnhancement.ts similarity index 100% rename from server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts rename to server/src/migrations/1708116312820-GeonamesEnhancement.ts diff --git a/server/src/infra/migrations/1708227417898-AddFileCreatedAtIndex.ts b/server/src/migrations/1708227417898-AddFileCreatedAtIndex.ts similarity index 100% rename from server/src/infra/migrations/1708227417898-AddFileCreatedAtIndex.ts rename to server/src/migrations/1708227417898-AddFileCreatedAtIndex.ts diff --git a/server/src/infra/migrations/1708425975121-RemoveExternalPath.ts b/server/src/migrations/1708425975121-RemoveExternalPath.ts similarity index 100% rename from server/src/infra/migrations/1708425975121-RemoveExternalPath.ts rename to server/src/migrations/1708425975121-RemoveExternalPath.ts diff --git a/server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts b/server/src/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts similarity index 100% rename from server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts rename to server/src/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts diff --git a/server/src/infra/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts b/server/src/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts similarity index 100% rename from server/src/infra/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts rename to server/src/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts diff --git a/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts b/server/src/migrations/1709763765506-AddExtensionToOriginalFileName.ts similarity index 100% rename from server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts rename to server/src/migrations/1709763765506-AddExtensionToOriginalFileName.ts diff --git a/server/src/infra/migrations/1709825430031-CascadeSharedLinksDelete.ts b/server/src/migrations/1709825430031-CascadeSharedLinksDelete.ts similarity index 100% rename from server/src/infra/migrations/1709825430031-CascadeSharedLinksDelete.ts rename to server/src/migrations/1709825430031-CascadeSharedLinksDelete.ts diff --git a/server/src/infra/migrations/1709870213078-AddUserStatus.ts b/server/src/migrations/1709870213078-AddUserStatus.ts similarity index 100% rename from server/src/infra/migrations/1709870213078-AddUserStatus.ts rename to server/src/migrations/1709870213078-AddUserStatus.ts diff --git a/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts b/server/src/migrations/1710182081326-AscendingOrderAlbum.ts similarity index 100% rename from server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts rename to server/src/migrations/1710182081326-AscendingOrderAlbum.ts diff --git a/server/src/infra/migrations/1710293990203-AddAssetRelationIndices.ts b/server/src/migrations/1710293990203-AddAssetRelationIndices.ts similarity index 100% rename from server/src/infra/migrations/1710293990203-AddAssetRelationIndices.ts rename to server/src/migrations/1710293990203-AddAssetRelationIndices.ts diff --git a/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts new file mode 100644 index 000000000..ab6f2a4e9 --- /dev/null +++ b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameWebpJpegPaths1711257900274 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'webpPath', 'thumbnailPath'); + await queryRunner.renameColumn('assets', 'resizePath', 'previewPath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'image.previewSize' + WHERE key = 'thumbnail.jpegSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.thumbnailSize' + WHERE key = 'thumbnail.webpSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.quality' + WHERE key = 'thumbnail.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.colorspace' + WHERE key = 'thumbnail.colorspace'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'thumbnailPath', 'webpPath'); + await queryRunner.renameColumn('assets', 'previewPath', 'resizePath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'thumbnail.jpegSize' + WHERE key = 'image.previewSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.webpSize' + WHERE key = 'image.thumbnailSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.quality' + WHERE key = 'image.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.colorspace' + WHERE key = 'image.colorspace'`, + ); + } +} diff --git a/server/src/migrations/1711637874206-AddMemoryTable.ts b/server/src/migrations/1711637874206-AddMemoryTable.ts new file mode 100644 index 000000000..6309cb508 --- /dev/null +++ b/server/src/migrations/1711637874206-AddMemoryTable.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddMemoryTable1711637874206 implements MigrationInterface { + name = 'AddMemoryTable1711637874206' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seenAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL, CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId"))`); + await queryRunner.query(`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId") `); + await queryRunner.query(`CREATE INDEX "IDX_6942ecf52d75d4273de19d2c16" ON "memories_assets_assets" ("assetsId") `); + await queryRunner.query(`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`); + await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`); + await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`); + await queryRunner.query(`DROP TABLE "memories_assets_assets"`); + await queryRunner.query(`DROP TABLE "memories"`); + } + +} diff --git a/server/src/infra/sql/access.repository.sql b/server/src/queries/access.repository.sql similarity index 96% rename from server/src/infra/sql/access.repository.sql rename to server/src/queries/access.repository.sql index a0c4e1927..0e1cab6d0 100644 --- a/server/src/infra/sql/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -196,6 +196,20 @@ WHERE ) AND ("LibraryEntity"."deletedAt" IS NULL) +-- AccessRepository.memory.checkOwnerAccess +SELECT + "MemoryEntity"."id" AS "MemoryEntity_id" +FROM + "memories" "MemoryEntity" +WHERE + ( + ( + ("MemoryEntity"."id" IN ($1)) + AND ("MemoryEntity"."ownerId" = $2) + ) + ) + AND ("MemoryEntity"."deletedAt" IS NULL) + -- AccessRepository.person.checkOwnerAccess SELECT "PersonEntity"."id" AS "PersonEntity_id" diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql new file mode 100644 index 000000000..306f71920 --- /dev/null +++ b/server/src/queries/activity.repository.sql @@ -0,0 +1,54 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- ActivityRepository.search +SELECT + "ActivityEntity"."id" AS "ActivityEntity_id", + "ActivityEntity"."createdAt" AS "ActivityEntity_createdAt", + "ActivityEntity"."updatedAt" AS "ActivityEntity_updatedAt", + "ActivityEntity"."albumId" AS "ActivityEntity_albumId", + "ActivityEntity"."userId" AS "ActivityEntity_userId", + "ActivityEntity"."assetId" AS "ActivityEntity_assetId", + "ActivityEntity"."comment" AS "ActivityEntity_comment", + "ActivityEntity"."isLiked" AS "ActivityEntity_isLiked", + "ActivityEntity__ActivityEntity_user"."id" AS "ActivityEntity__ActivityEntity_user_id", + "ActivityEntity__ActivityEntity_user"."name" AS "ActivityEntity__ActivityEntity_user_name", + "ActivityEntity__ActivityEntity_user"."avatarColor" AS "ActivityEntity__ActivityEntity_user_avatarColor", + "ActivityEntity__ActivityEntity_user"."isAdmin" AS "ActivityEntity__ActivityEntity_user_isAdmin", + "ActivityEntity__ActivityEntity_user"."email" AS "ActivityEntity__ActivityEntity_user_email", + "ActivityEntity__ActivityEntity_user"."storageLabel" AS "ActivityEntity__ActivityEntity_user_storageLabel", + "ActivityEntity__ActivityEntity_user"."oauthId" AS "ActivityEntity__ActivityEntity_user_oauthId", + "ActivityEntity__ActivityEntity_user"."profileImagePath" AS "ActivityEntity__ActivityEntity_user_profileImagePath", + "ActivityEntity__ActivityEntity_user"."shouldChangePassword" AS "ActivityEntity__ActivityEntity_user_shouldChangePassword", + "ActivityEntity__ActivityEntity_user"."createdAt" AS "ActivityEntity__ActivityEntity_user_createdAt", + "ActivityEntity__ActivityEntity_user"."deletedAt" AS "ActivityEntity__ActivityEntity_user_deletedAt", + "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status", + "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt", + "ActivityEntity__ActivityEntity_user"."memoriesEnabled" AS "ActivityEntity__ActivityEntity_user_memoriesEnabled", + "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes", + "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes" +FROM + "activity" "ActivityEntity" + LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId" + AND ( + "ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL + ) +WHERE + (("ActivityEntity"."albumId" = $1)) +ORDER BY + "ActivityEntity"."createdAt" ASC + +-- ActivityRepository.getStatistics +SELECT + COUNT(DISTINCT ("ActivityEntity"."id")) AS "cnt" +FROM + "activity" "ActivityEntity" + LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId" + AND ( + "ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL + ) +WHERE + ( + ("ActivityEntity"."assetId" = $1) + AND ("ActivityEntity"."albumId" = $2) + AND ("ActivityEntity"."isLiked" = $3) + ) diff --git a/server/src/infra/sql/album.repository.sql b/server/src/queries/album.repository.sql similarity index 99% rename from server/src/infra/sql/album.repository.sql rename to server/src/queries/album.repository.sql index ddedc0095..50f775d2f 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -590,7 +590,7 @@ DELETE FROM "albums_assets_assets" WHERE "albums_assets_assets"."assetsId" = $1 --- AlbumRepository.removeAssets +-- AlbumRepository.removeAssetIds DELETE FROM "albums_assets_assets" WHERE ( @@ -646,7 +646,7 @@ WHERE LIMIT 1 --- AlbumRepository.addAssets +-- AlbumRepository.addAssetIds INSERT INTO "albums_assets_assets" ("albumsId", "assetsId") VALUES diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/queries/api.key.repository.sql similarity index 100% rename from server/src/infra/sql/api.key.repository.sql rename to server/src/queries/api.key.repository.sql diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/queries/asset.repository.sql similarity index 82% rename from server/src/infra/sql/asset.repository.sql rename to server/src/queries/asset.repository.sql index e230a7346..6f0e8cd5e 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -1,80 +1,87 @@ -- NOTE: This file is auto generated by ./sql-generator --- AssetRepository.getByDate +-- AssetRepository.getByDayOfYear SELECT - "AssetEntity"."id" AS "AssetEntity_id", - "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", - "AssetEntity"."ownerId" AS "AssetEntity_ownerId", - "AssetEntity"."libraryId" AS "AssetEntity_libraryId", - "AssetEntity"."deviceId" AS "AssetEntity_deviceId", - "AssetEntity"."type" AS "AssetEntity_type", - "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", - "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", - "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", - "AssetEntity"."createdAt" AS "AssetEntity_createdAt", - "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", - "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", - "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", - "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", - "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", - "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", - "AssetEntity"."isArchived" AS "AssetEntity_isArchived", - "AssetEntity"."isExternal" AS "AssetEntity_isExternal", - "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", - "AssetEntity"."isOffline" AS "AssetEntity_isOffline", - "AssetEntity"."checksum" AS "AssetEntity_checksum", - "AssetEntity"."duration" AS "AssetEntity_duration", - "AssetEntity"."isVisible" AS "AssetEntity_isVisible", - "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", - "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", - "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", - "AssetEntity"."stackId" AS "AssetEntity_stackId", - "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", - "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", - "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", - "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", - "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", - "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", - "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", - "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", - "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", - "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", - "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", - "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", - "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", - "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", - "AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId", - "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", - "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", - "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", - "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", - "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", - "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", - "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", - "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", - "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", - "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", - "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", - "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", - "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps" + "entity"."id" AS "entity_id", + "entity"."deviceAssetId" AS "entity_deviceAssetId", + "entity"."ownerId" AS "entity_ownerId", + "entity"."libraryId" AS "entity_libraryId", + "entity"."deviceId" AS "entity_deviceId", + "entity"."type" AS "entity_type", + "entity"."originalPath" AS "entity_originalPath", + "entity"."previewPath" AS "entity_previewPath", + "entity"."thumbnailPath" AS "entity_thumbnailPath", + "entity"."thumbhash" AS "entity_thumbhash", + "entity"."encodedVideoPath" AS "entity_encodedVideoPath", + "entity"."createdAt" AS "entity_createdAt", + "entity"."updatedAt" AS "entity_updatedAt", + "entity"."deletedAt" AS "entity_deletedAt", + "entity"."fileCreatedAt" AS "entity_fileCreatedAt", + "entity"."localDateTime" AS "entity_localDateTime", + "entity"."fileModifiedAt" AS "entity_fileModifiedAt", + "entity"."isFavorite" AS "entity_isFavorite", + "entity"."isArchived" AS "entity_isArchived", + "entity"."isExternal" AS "entity_isExternal", + "entity"."isReadOnly" AS "entity_isReadOnly", + "entity"."isOffline" AS "entity_isOffline", + "entity"."checksum" AS "entity_checksum", + "entity"."duration" AS "entity_duration", + "entity"."isVisible" AS "entity_isVisible", + "entity"."livePhotoVideoId" AS "entity_livePhotoVideoId", + "entity"."originalFileName" AS "entity_originalFileName", + "entity"."sidecarPath" AS "entity_sidecarPath", + "entity"."stackId" AS "entity_stackId", + "exifInfo"."assetId" AS "exifInfo_assetId", + "exifInfo"."description" AS "exifInfo_description", + "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", + "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", + "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", + "exifInfo"."orientation" AS "exifInfo_orientation", + "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", + "exifInfo"."modifyDate" AS "exifInfo_modifyDate", + "exifInfo"."timeZone" AS "exifInfo_timeZone", + "exifInfo"."latitude" AS "exifInfo_latitude", + "exifInfo"."longitude" AS "exifInfo_longitude", + "exifInfo"."projectionType" AS "exifInfo_projectionType", + "exifInfo"."city" AS "exifInfo_city", + "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", + "exifInfo"."autoStackId" AS "exifInfo_autoStackId", + "exifInfo"."state" AS "exifInfo_state", + "exifInfo"."country" AS "exifInfo_country", + "exifInfo"."make" AS "exifInfo_make", + "exifInfo"."model" AS "exifInfo_model", + "exifInfo"."lensModel" AS "exifInfo_lensModel", + "exifInfo"."fNumber" AS "exifInfo_fNumber", + "exifInfo"."focalLength" AS "exifInfo_focalLength", + "exifInfo"."iso" AS "exifInfo_iso", + "exifInfo"."exposureTime" AS "exifInfo_exposureTime", + "exifInfo"."profileDescription" AS "exifInfo_profileDescription", + "exifInfo"."colorspace" AS "exifInfo_colorspace", + "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."fps" AS "exifInfo_fps" FROM - "assets" "AssetEntity" - LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" + "assets" "entity" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" WHERE ( - ( - ("AssetEntity"."ownerId" = $1) - AND ("AssetEntity"."isVisible" = $2) - AND ("AssetEntity"."isArchived" = $3) - AND (NOT ("AssetEntity"."resizePath" IS NULL)) - AND ("AssetEntity"."fileCreatedAt" BETWEEN $4 AND $5) - ) + "entity"."ownerId" IN ($1) + AND "entity"."isVisible" = true + AND "entity"."isArchived" = false + AND "entity"."previewPath" IS NOT NULL + AND EXTRACT( + DAY + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $2 + AND EXTRACT( + MONTH + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $3 ) - AND ("AssetEntity"."deletedAt" IS NULL) + AND ("entity"."deletedAt" IS NULL) ORDER BY - "AssetEntity"."fileCreatedAt" DESC + "entity"."localDateTime" ASC -- AssetRepository.getByIds SELECT @@ -85,8 +92,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -121,8 +128,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -206,8 +213,8 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."resizePath" AS "bd93d5747511a4dad4923546c51365bf1a803774_resizePath", - "bd93d5747511a4dad4923546c51365bf1a803774"."webpPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_webpPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -246,6 +253,34 @@ DELETE FROM "assets" WHERE "ownerId" = $1 +-- AssetRepository.getLibraryAssetPaths +SELECT DISTINCT + "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" +FROM + ( + SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline" + FROM + "assets" "AssetEntity" + LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" + AND ( + "AssetEntity__AssetEntity_library"."deletedAt" IS NULL + ) + WHERE + ( + ( + ((("AssetEntity__AssetEntity_library"."id" = $1))) + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "AssetEntity_id" ASC +LIMIT + 2 + -- AssetRepository.getByLibraryIdAndOriginalPath SELECT DISTINCT "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" @@ -259,8 +294,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -305,7 +340,7 @@ LIMIT WITH paths AS ( SELECT - unnest($2::text []) AS path + unnest($2::text[]) AS path ) SELECT path @@ -356,8 +391,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -402,8 +437,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -446,8 +481,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -535,8 +570,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -594,8 +629,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -706,8 +741,8 @@ LIMIT SELECT asset.*, e.*, - COALESCE("si"."tags", array[]::text []) AS "tags", - COALESCE("si"."objects", array[]::text []) AS "objects" + COALESCE("si"."tags", array[]::text[]) AS "tags", + COALESCE("si"."objects", array[]::text[]) AS "objects" FROM "assets" "asset" INNER JOIN "exif" "e" ON asset."id" = e."assetId" diff --git a/server/src/infra/sql/library.repository.sql b/server/src/queries/library.repository.sql similarity index 100% rename from server/src/infra/sql/library.repository.sql rename to server/src/queries/library.repository.sql diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql new file mode 100644 index 000000000..aa3df240c --- /dev/null +++ b/server/src/queries/memory.repository.sql @@ -0,0 +1,18 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- MemoryRepository.getAssetIds +SELECT + "memories_assets"."assetsId" AS "assetId" +FROM + "memories_assets_assets" "memories_assets" +WHERE + "memories_assets"."memoriesId" = $1 + AND "memories_assets"."assetsId" IN ($2) + +-- MemoryRepository.removeAssetIds +DELETE FROM "memories_assets_assets" +WHERE + ( + "memoriesId" = $1 + AND "assetsId" IN ($2) + ) diff --git a/server/src/queries/metadata.repository.sql b/server/src/queries/metadata.repository.sql new file mode 100644 index 000000000..bed7d59ab --- /dev/null +++ b/server/src/queries/metadata.repository.sql @@ -0,0 +1,66 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- MetadataRepository.getCountries +SELECT DISTINCT + ON ("exif"."country") "exif"."country" AS "exif_country", + "exif"."assetId" AS "exif_assetId" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND "exif"."country" IS NOT NULL + +-- MetadataRepository.getStates +SELECT DISTINCT + ON ("exif"."state") "exif"."state" AS "exif_state", + "exif"."assetId" AS "exif_assetId" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND "exif"."state" IS NOT NULL + AND "exif"."country" = $2 + +-- MetadataRepository.getCities +SELECT DISTINCT + ON ("exif"."city") "exif"."city" AS "exif_city", + "exif"."assetId" AS "exif_assetId" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND "exif"."city" IS NOT NULL + AND "exif"."country" = $2 + AND "exif"."state" = $3 + +-- MetadataRepository.getCameraMakes +SELECT DISTINCT + ON ("exif"."make") "exif"."make" AS "exif_make", + "exif"."assetId" AS "exif_assetId" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND "exif"."make" IS NOT NULL + AND "exif"."model" = $2 + +-- MetadataRepository.getCameraModels +SELECT DISTINCT + ON ("exif"."model") "exif"."model" AS "exif_model", + "exif"."assetId" AS "exif_assetId" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND "exif"."model" IS NOT NULL + AND "exif"."make" = $2 diff --git a/server/src/infra/sql/move.repository.sql b/server/src/queries/move.repository.sql similarity index 100% rename from server/src/infra/sql/move.repository.sql rename to server/src/queries/move.repository.sql diff --git a/server/src/infra/sql/person.repository.sql b/server/src/queries/person.repository.sql similarity index 97% rename from server/src/infra/sql/person.repository.sql rename to server/src/queries/person.repository.sql index b6a513ff9..1cde746d8 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -152,8 +152,8 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -250,8 +250,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -380,8 +380,8 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/infra/sql/search.repository.sql b/server/src/queries/search.repository.sql similarity index 71% rename from server/src/infra/sql/search.repository.sql rename to server/src/queries/search.repository.sql index a11f8805a..3e83d7238 100644 --- a/server/src/infra/sql/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,8 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -45,8 +45,8 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -110,8 +110,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -141,8 +141,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -254,15 +254,123 @@ WHERE OR f_unaccent ("admin1Name") %>> f_unaccent ($1) OR f_unaccent ("alternateNames") %>> f_unaccent ($1) ORDER BY - COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0) + COALESCE( + COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0.1) + COALESCE( f_unaccent ("admin2Name") <->>> f_unaccent ($1), - 0 + 0.1 ) + COALESCE( f_unaccent ("admin1Name") <->>> f_unaccent ($1), - 0 + 0.1 ) + COALESCE( f_unaccent ("alternateNames") <->>> f_unaccent ($1), - 0 + 0.1 ) ASC LIMIT 20 + +-- SearchRepository.getAssetsByCity +WITH RECURSIVE + cte AS ( + ( + SELECT + city, + "assetId" + FROM + exif + INNER JOIN assets ON exif."assetId" = assets.id + WHERE + "ownerId" = ANY ($1::uuid []) + AND "isVisible" = $2 + AND "isArchived" = $3 + AND type = $4 + ORDER BY + city + LIMIT + 1 + ) + UNION ALL + SELECT + l.city, + l."assetId" + FROM + cte c, + LATERAL ( + SELECT + city, + "assetId" + FROM + exif + INNER JOIN assets ON exif."assetId" = assets.id + WHERE + city > c.city + AND "ownerId" = ANY ($1::uuid []) + AND "isVisible" = $2 + AND "isArchived" = $3 + AND type = $4 + ORDER BY + city + LIMIT + 1 + ) l + ) +SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "exif"."assetId" AS "exif_assetId", + "exif"."description" AS "exif_description", + "exif"."exifImageWidth" AS "exif_exifImageWidth", + "exif"."exifImageHeight" AS "exif_exifImageHeight", + "exif"."fileSizeInByte" AS "exif_fileSizeInByte", + "exif"."orientation" AS "exif_orientation", + "exif"."dateTimeOriginal" AS "exif_dateTimeOriginal", + "exif"."modifyDate" AS "exif_modifyDate", + "exif"."timeZone" AS "exif_timeZone", + "exif"."latitude" AS "exif_latitude", + "exif"."longitude" AS "exif_longitude", + "exif"."projectionType" AS "exif_projectionType", + "exif"."city" AS "exif_city", + "exif"."livePhotoCID" AS "exif_livePhotoCID", + "exif"."autoStackId" AS "exif_autoStackId", + "exif"."state" AS "exif_state", + "exif"."country" AS "exif_country", + "exif"."make" AS "exif_make", + "exif"."model" AS "exif_model", + "exif"."lensModel" AS "exif_lensModel", + "exif"."fNumber" AS "exif_fNumber", + "exif"."focalLength" AS "exif_focalLength", + "exif"."iso" AS "exif_iso", + "exif"."exposureTime" AS "exif_exposureTime", + "exif"."profileDescription" AS "exif_profileDescription", + "exif"."colorspace" AS "exif_colorspace", + "exif"."bitsPerSample" AS "exif_bitsPerSample", + "exif"."fps" AS "exif_fps" +FROM + "assets" "asset" + INNER JOIN "exif" "exif" ON "exif"."assetId" = "asset"."id" + INNER JOIN cte ON asset.id = cte."assetId" diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql similarity index 97% rename from server/src/infra/sql/shared.link.repository.sql rename to server/src/queries/shared.link.repository.sql index 27531cfc9..78581b8ba 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -95,8 +95,8 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -218,8 +218,8 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/infra/sql/system.config.repository.sql b/server/src/queries/system.config.repository.sql similarity index 100% rename from server/src/infra/sql/system.config.repository.sql rename to server/src/queries/system.config.repository.sql diff --git a/server/src/infra/sql/user.repository.sql b/server/src/queries/user.repository.sql similarity index 100% rename from server/src/infra/sql/user.repository.sql rename to server/src/queries/user.repository.sql diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/queries/user.token.repository.sql similarity index 100% rename from server/src/infra/sql/user.token.repository.sql rename to server/src/queries/user.token.repository.sql diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/repositories/access.repository.ts similarity index 89% rename from server/src/infra/repositories/access.repository.ts rename to server/src/repositories/access.repository.ts index ad650bf0e..fd74eb2ec 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -1,20 +1,18 @@ -import { IAccessRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; +import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { LibraryEntity } from 'src/entities/library.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { PartnerEntity } from 'src/entities/partner.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; -import { - ActivityEntity, - AlbumEntity, - AssetEntity, - AssetFaceEntity, - LibraryEntity, - PartnerEntity, - PersonEntity, - SharedLinkEntity, - UserTokenEntity, -} from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { ChunkedSet } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; @@ -22,6 +20,7 @@ type IAssetAccess = IAccessRepository['asset']; type IAuthDeviceAccess = IAccessRepository['authDevice']; type ILibraryAccess = IAccessRepository['library']; type ITimelineAccess = IAccessRepository['timeline']; +type IMemoryAccess = IAccessRepository['memory']; type IPersonAccess = IAccessRepository['person']; type IPartnerAccess = IAccessRepository['partner']; @@ -348,6 +347,28 @@ class TimelineAccess implements ITimelineAccess { } } +class MemoryAccess implements IMemoryAccess { + constructor(private memoryRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, memoryIds: Set): Promise> { + if (memoryIds.size === 0) { + return new Set(); + } + + return this.memoryRepository + .find({ + select: { id: true }, + where: { + id: In([...memoryIds]), + ownerId: userId, + }, + }) + .then((memories) => new Set(memories.map((memory) => memory.id))); + } +} + class PersonAccess implements IPersonAccess { constructor( private assetFaceRepository: Repository, @@ -419,6 +440,7 @@ export class AccessRepository implements IAccessRepository { asset: IAssetAccess; authDevice: IAuthDeviceAccess; library: ILibraryAccess; + memory: IMemoryAccess; person: IPersonAccess; partner: IPartnerAccess; timeline: ITimelineAccess; @@ -428,6 +450,7 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(AssetEntity) assetRepository: Repository, @InjectRepository(AlbumEntity) albumRepository: Repository, @InjectRepository(LibraryEntity) libraryRepository: Repository, + @InjectRepository(MemoryEntity) memoryRepository: Repository, @InjectRepository(PartnerEntity) partnerRepository: Repository, @InjectRepository(PersonEntity) personRepository: Repository, @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @@ -439,6 +462,7 @@ export class AccessRepository implements IAccessRepository { this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); this.authDevice = new AuthDeviceAccess(tokenRepository); this.library = new LibraryAccess(libraryRepository); + this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); this.timeline = new TimelineAccess(partnerRepository); diff --git a/server/src/infra/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts similarity index 86% rename from server/src/infra/repositories/activity.repository.ts rename to server/src/repositories/activity.repository.ts index c546056db..475ad3b85 100644 --- a/server/src/infra/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -1,10 +1,10 @@ -import { IActivityRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Repository } from 'typeorm'; -import { ActivityEntity } from '../entities/activity.entity'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; export interface ActivitySearch { albumId?: string; diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/repositories/album.repository.ts similarity index 93% rename from server/src/infra/repositories/album.repository.ts rename to server/src/repositories/album.repository.ts index 60ef6126c..bbaab2a12 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -1,14 +1,14 @@ -import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import _ from 'lodash'; +import { dataSource } from 'src/database.config'; +import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AlbumAsset, AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { setUnion } from 'src/utils/set'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; -import { setUnion } from '../../domain/domain.util'; -import { dataSource } from '../database.config'; -import { AlbumEntity, AssetEntity } from '../entities'; -import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util'; -import { Chunked, ChunkedArray } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() @@ -197,7 +197,7 @@ export class AlbumRepository implements IAlbumRepository { @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @Chunked({ paramIndex: 1 }) - async removeAssets(albumId: string, assetIds: string[]): Promise { + async removeAssetIds(albumId: string, assetIds: string[]): Promise { await this.dataSource .createQueryBuilder() .delete() @@ -254,8 +254,8 @@ export class AlbumRepository implements IAlbumRepository { }); } - @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] }) - async addAssets({ albumId, assetIds }: AlbumAssets): Promise { + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + async addAssetIds(albumId: string, assetIds: string[]): Promise { await this.dataSource .createQueryBuilder() .insert() diff --git a/server/src/infra/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts similarity index 85% rename from server/src/infra/repositories/api-key.repository.ts rename to server/src/repositories/api-key.repository.ts index 3cafc22eb..d03d04806 100644 --- a/server/src/infra/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -1,10 +1,10 @@ -import { IKeyRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { APIKeyEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/asset-stack.repository.ts b/server/src/repositories/asset-stack.repository.ts similarity index 84% rename from server/src/infra/repositories/asset-stack.repository.ts rename to server/src/repositories/asset-stack.repository.ts index d769030bb..660dfbe47 100644 --- a/server/src/infra/repositories/asset-stack.repository.ts +++ b/server/src/repositories/asset-stack.repository.ts @@ -1,9 +1,9 @@ -import { IAssetStackRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { AssetStackEntity } from 'src/entities/asset-stack.entity'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { AssetStackEntity } from '../entities'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/repositories/asset-v1.repository.ts similarity index 70% rename from server/src/immich/api-v1/asset/asset-repository.ts rename to server/src/repositories/asset-v1.repository.ts index 18feb65dc..229e700fd 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/repositories/asset-v1.repository.ts @@ -1,42 +1,16 @@ -import { AssetEntity, ExifEntity } from '@app/infra/entities'; -import { OptionalBetween } from '@app/infra/infra.utils'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { OptionalBetween } from 'src/utils/database'; import { In } from 'typeorm/find-options/operator/In.js'; import { Repository } from 'typeorm/repository/Repository.js'; -import { AssetSearchDto } from './dto/asset-search.dto'; -import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { SearchPropertiesDto } from './dto/search-properties.dto'; -import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; -export interface AssetCheck { - id: string; - checksum: Buffer; -} - -export interface AssetOwnerCheck extends AssetCheck { - ownerId: string; -} - -export interface IAssetRepositoryV1 { - get(id: string): Promise; - getLocationsByUserId(userId: string): Promise; - getDetectedObjectsByUserId(userId: string): Promise; - getAllByUserId(userId: string, dto: AssetSearchDto): Promise; - getSearchPropertiesByUserId(userId: string): Promise; - getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; - getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; - getByOriginalPath(originalPath: string): Promise; -} - -export const IAssetRepositoryV1 = 'IAssetRepositoryV1'; @Injectable() export class AssetRepositoryV1 implements IAssetRepositoryV1 { - constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(ExifEntity) private exifRepository: Repository, - ) {} + constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} /** * Retrieves all assets by user ID. @@ -92,7 +66,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getDetectedObjectsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN smart_info si ON a.id = si."assetId" WHERE a."ownerId" = $1 @@ -106,7 +80,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getLocationsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (e.city) a.id, e.city, a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN exif e ON a.id = e."assetId" WHERE a."ownerId" = $1 diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts similarity index 89% rename from server/src/infra/repositories/asset.repository.ts rename to server/src/repositories/asset.repository.ts index 09cf2c779..e6389c2e5 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,31 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import path from 'node:path'; +import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetOrder } from 'src/entities/album.entity'; +import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { AssetBuilderOptions, AssetCreate, AssetExploreFieldOptions, AssetPathEntity, - AssetSearchOptions, AssetStats, AssetStatsOptions, + AssetUpdateAllOptions, + AssetUpdateOptions, IAssetRepository, LivePhotoSearchOptions, MapMarker, MapMarkerSearchOptions, MetadataSearchOptions, MonthDay, - Paginated, - PaginationMode, - PaginationOptions, - SearchExploreItem, TimeBucketItem, TimeBucketOptions, TimeBucketSize, WithProperty, WithoutProperty, -} from '@app/domain'; -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { DateTime } from 'luxon'; -import path from 'node:path'; +} from 'src/interfaces/asset.interface'; +import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; +import { OptionalBetween, searchAssetBuilder } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { Brackets, FindOptionsRelations, @@ -36,10 +42,6 @@ import { Not, Repository, } from 'typeorm'; -import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; const truncateMap: Record = { [TimeBucketSize.DAY]: 'day', @@ -73,42 +75,7 @@ export class AssetRepository implements IAssetRepository { return this.repository.save(asset); } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] }) - getByDate(ownerId: string, date: Date): Promise { - // For reference of a correct approach although slower - - // let builder = this.repository - // .createQueryBuilder('asset') - // .leftJoin('asset.exifInfo', 'exifInfo') - // .where('asset.ownerId = :ownerId', { ownerId }) - // .andWhere( - // `coalesce(date_trunc('day', asset."fileCreatedAt", "exifInfo"."timeZone") at TIME ZONE "exifInfo"."timeZone", date_trunc('day', asset."fileCreatedAt")) IN (:date)`, - // { date }, - // ) - // .andWhere('asset.isVisible = true') - // .andWhere('asset.isArchived = false') - // .orderBy('asset.fileCreatedAt', 'DESC'); - - // return builder.getMany(); - - return this.repository.find({ - where: { - ownerId, - isVisible: true, - isArchived: false, - resizePath: Not(IsNull()), - fileCreatedAt: OptionalBetween(date, DateTime.fromJSDate(date).plus({ day: 1 }).toJSDate()), - }, - relations: { - exifInfo: true, - }, - order: { - fileCreatedAt: 'DESC', - }, - }); - } - - @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) + @GenerateSql({ params: [[DummyValue.UUID], { day: 1, month: 1 }] }) getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { return this.repository .createQueryBuilder('entity') @@ -116,7 +83,7 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.resizePath IS NOT NULL + AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -126,7 +93,7 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') - .orderBy('entity.localDateTime', 'DESC') + .orderBy('entity.localDateTime', 'ASC') .getMany(); } @@ -192,7 +159,7 @@ export class AssetRepository implements IAssetRepository { return this.getAll(pagination, { ...options, userIds: [userId] }); } - @GenerateSql({ params: [[DummyValue.UUID]] }) + @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated { return paginate(this.repository, pagination, { select: { id: true, originalPath: true, isOffline: true }, @@ -275,7 +242,7 @@ export class AssetRepository implements IAssetRepository { @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) @Chunked() - async updateAll(ids: string[], options: Partial): Promise { + async updateAll(ids: string[], options: AssetUpdateAllOptions): Promise { await this.repository.update({ id: In(ids) }, options); } @@ -289,21 +256,8 @@ export class AssetRepository implements IAssetRepository { await this.repository.restore({ id: In(ids) }); } - async save(asset: Partial): Promise { - const { id } = await this.repository.save(asset); - return this.repository.findOneOrFail({ - where: { id }, - relations: { - exifInfo: true, - owner: true, - smartInfo: true, - tags: true, - faces: { - person: true, - }, - }, - withDeleted: true, - }); + async update(asset: AssetUpdateOptions): Promise { + await this.repository.update(asset.id, asset); } async remove(asset: AssetEntity): Promise { @@ -348,10 +302,10 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { where = [ - { resizePath: IsNull(), isVisible: true }, - { resizePath: '', isVisible: true }, - { webpPath: IsNull(), isVisible: true }, - { webpPath: '', isVisible: true }, + { previewPath: IsNull(), isVisible: true }, + { previewPath: '', isVisible: true }, + { thumbnailPath: IsNull(), isVisible: true }, + { thumbnailPath: '', isVisible: true }, { thumbhash: IsNull(), isVisible: true }, ]; break; @@ -385,7 +339,7 @@ export class AssetRepository implements IAssetRepository { }; where = { isVisible: true, - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), smartSearch: { embedding: IsNull(), }, @@ -398,7 +352,7 @@ export class AssetRepository implements IAssetRepository { smartInfo: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, smartInfo: { tags: IsNull(), @@ -413,7 +367,7 @@ export class AssetRepository implements IAssetRepository { jobStatus: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: IsNull(), @@ -431,7 +385,7 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: Not(IsNull()), diff --git a/server/src/infra/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts similarity index 81% rename from server/src/infra/repositories/audit.repository.ts rename to server/src/repositories/audit.repository.ts index bc00cbe9a..50f5631f3 100644 --- a/server/src/infra/repositories/audit.repository.ts +++ b/server/src/repositories/audit.repository.ts @@ -1,8 +1,8 @@ -import { AuditSearch, IAuditRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { LessThan, MoreThan, Repository } from 'typeorm'; -import { AuditEntity } from '../entities'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() export class AuditRepository implements IAuditRepository { diff --git a/server/src/infra/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts similarity index 83% rename from server/src/infra/repositories/crypto.repository.ts rename to server/src/repositories/crypto.repository.ts index f98fa9d87..9102715a1 100644 --- a/server/src/infra/repositories/crypto.repository.ts +++ b/server/src/repositories/crypto.repository.ts @@ -1,9 +1,9 @@ -import { ICryptoRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { compareSync, hash } from 'bcrypt'; import { createHash, randomBytes, randomUUID } from 'node:crypto'; import { createReadStream } from 'node:fs'; -import { Instrumentation } from '../instrumentation'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; @Instrumentation() @Injectable() @@ -41,4 +41,8 @@ export class CryptoRepository implements ICryptoRepository { stream.on('end', () => resolve(hash.digest())); }); } + + newPassword(bytes: number) { + return randomBytes(bytes).toString('base64').replaceAll(/\W/g, ''); + } } diff --git a/server/src/infra/repositories/database.repository.ts b/server/src/repositories/database.repository.ts similarity index 96% rename from server/src/infra/repositories/database.repository.ts rename to server/src/repositories/database.repository.ts index 8154e9122..4ff24eeaa 100644 --- a/server/src/infra/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,3 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import AsyncLock from 'async-lock'; +import { vectorExt } from 'src/database.config'; import { DatabaseExtension, DatabaseLock, @@ -5,18 +9,13 @@ import { VectorExtension, VectorIndex, VectorUpdateResult, - Version, - VersionType, extName, -} from '@app/domain'; -import { vectorExt } from '@app/infra/database.config'; -import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; -import AsyncLock from 'async-lock'; +} from 'src/interfaces/database.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; +import { isValidInteger } from 'src/validation'; import { DataSource, EntityManager, QueryRunner } from 'typeorm'; -import { isValidInteger } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; -import { ImmichLogger } from '../logger'; @Instrumentation() @Injectable() diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts new file mode 100644 index 000000000..be1de76c2 --- /dev/null +++ b/server/src/repositories/event.repository.ts @@ -0,0 +1,88 @@ +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + OnGatewayConnection, + OnGatewayDisconnect, + OnGatewayInit, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { + ClientEventMap, + IEventRepository, + ServerAsyncEventMap, + ServerEvent, + ServerEventMap, +} from 'src/interfaces/event.interface'; +import { AuthService } from 'src/services/auth.service'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; + +@Instrumentation() +@WebSocketGateway({ + cors: true, + path: '/api/socket.io', + transports: ['websocket'], +}) +export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository { + private logger = new ImmichLogger(EventRepository.name); + + @WebSocketServer() + private server?: Server; + + constructor( + private authService: AuthService, + private eventEmitter: EventEmitter2, + ) {} + + afterInit(server: Server) { + this.logger.log('Initialized websocket server'); + + for (const event of Object.values(ServerEvent)) { + if (event === ServerEvent.WEBSOCKET_CONNECT) { + continue; + } + + server.on(event, (data: unknown) => { + this.logger.debug(`Server event: ${event} (receive)`); + this.eventEmitter.emit(event, data); + }); + } + } + + async handleConnection(client: Socket) { + try { + this.logger.log(`Websocket Connect: ${client.id}`); + const auth = await this.authService.validate(client.request.headers, {}); + await client.join(auth.user.id); + this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id }); + } catch (error: Error | any) { + this.logger.error(`Websocket connection error: ${error}`, error?.stack); + client.emit('error', 'unauthorized'); + client.disconnect(); + } + } + + async handleDisconnect(client: Socket) { + this.logger.log(`Websocket Disconnect: ${client.id}`); + await client.leave(client.nsp.name); + } + + clientSend(event: E, userId: string, data: ClientEventMap[E]) { + this.server?.to(userId).emit(event, data); + } + + clientBroadcast(event: E, data: ClientEventMap[E]) { + this.server?.emit(event, data); + } + + serverSend(event: E, data: ServerEventMap[E]) { + this.logger.debug(`Server event: ${event} (send)`); + this.server?.serverSideEmit(event, data); + return this.eventEmitter.emit(event, data); + } + + serverSendAsync(event: E, data: ServerAsyncEventMap[E]): Promise { + return this.eventEmitter.emitAsync(event, data) as Promise; + } +} diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts new file mode 100644 index 000000000..336d5df0f --- /dev/null +++ b/server/src/repositories/index.ts @@ -0,0 +1,93 @@ +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IMetricRepository } from 'src/interfaces/metric.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumRepository } from 'src/repositories/album.repository'; +import { ApiKeyRepository } from 'src/repositories/api-key.repository'; +import { AssetStackRepository } from 'src/repositories/asset-stack.repository'; +import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { AuditRepository } from 'src/repositories/audit.repository'; +import { CryptoRepository } from 'src/repositories/crypto.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { EventRepository } from 'src/repositories/event.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LibraryRepository } from 'src/repositories/library.repository'; +import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; +import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { MetricRepository } from 'src/repositories/metric.repository'; +import { MoveRepository } from 'src/repositories/move.repository'; +import { PartnerRepository } from 'src/repositories/partner.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { SearchRepository } from 'src/repositories/search.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemConfigRepository } from 'src/repositories/system-config.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { TagRepository } from 'src/repositories/tag.repository'; +import { UserTokenRepository } from 'src/repositories/user-token.repository'; +import { UserRepository } from 'src/repositories/user.repository'; + +export const repositories = [ + { provide: IActivityRepository, useClass: ActivityRepository }, + { provide: IAccessRepository, useClass: AccessRepository }, + { provide: IAlbumRepository, useClass: AlbumRepository }, + { provide: IAssetRepository, useClass: AssetRepository }, + { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 }, + { provide: IAssetStackRepository, useClass: AssetStackRepository }, + { provide: IAuditRepository, useClass: AuditRepository }, + { provide: ICryptoRepository, useClass: CryptoRepository }, + { provide: IDatabaseRepository, useClass: DatabaseRepository }, + { provide: IEventRepository, useClass: EventRepository }, + { provide: IJobRepository, useClass: JobRepository }, + { provide: ILibraryRepository, useClass: LibraryRepository }, + { provide: IKeyRepository, useClass: ApiKeyRepository }, + { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, + { provide: IMemoryRepository, useClass: MemoryRepository }, + { provide: IMetadataRepository, useClass: MetadataRepository }, + { provide: IMetricRepository, useClass: MetricRepository }, + { provide: IMoveRepository, useClass: MoveRepository }, + { provide: IPartnerRepository, useClass: PartnerRepository }, + { provide: IPersonRepository, useClass: PersonRepository }, + { provide: IServerInfoRepository, useClass: ServerInfoRepository }, + { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, + { provide: ISearchRepository, useClass: SearchRepository }, + { provide: IStorageRepository, useClass: StorageRepository }, + { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, + { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, + { provide: ITagRepository, useClass: TagRepository }, + { provide: IMediaRepository, useClass: MediaRepository }, + { provide: IUserRepository, useClass: UserRepository }, + { provide: IUserTokenRepository, useClass: UserTokenRepository }, +]; diff --git a/server/src/infra/repositories/job.repository.ts b/server/src/repositories/job.repository.ts similarity index 66% rename from server/src/infra/repositories/job.repository.ts rename to server/src/repositories/job.repository.ts index 227967a07..a7c99f93c 100644 --- a/server/src/infra/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -1,14 +1,3 @@ -import { - IJobRepository, - JobCounts, - JobItem, - JobName, - JOBS_TO_QUEUE, - QueueCleanType, - QueueName, - QueueStatus, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; import { getQueueToken } from '@nestjs/bullmq'; import { Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; @@ -16,8 +5,79 @@ import { SchedulerRegistry } from '@nestjs/schedule'; import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq'; import { CronJob, CronTime } from 'cron'; import { setTimeout } from 'node:timers/promises'; -import { bullConfig } from '../infra.config'; -import { Instrumentation } from '../instrumentation'; +import { bullConfig } from 'src/config'; +import { + IJobRepository, + JobCounts, + JobItem, + JobName, + QueueCleanType, + QueueName, + QueueStatus, +} from 'src/interfaces/job.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; + +export const JOBS_TO_QUEUE: Record = { + // misc + [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK, + [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK, + [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK, + [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK, + [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK, + [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, + [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, + [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK, + + // conversion + [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, + [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, + + // thumbnails + [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + + // metadata + [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, + [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, + [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION, + + // storage template + [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION, + [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION, + + // migration + [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION, + [JobName.MIGRATE_ASSET]: QueueName.MIGRATION, + [JobName.MIGRATE_PERSON]: QueueName.MIGRATION, + + // facial recognition + [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION, + [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION, + [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, + [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, + + // smart search + [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH, + [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH, + + // XMP sidecars + [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, + [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, + [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, + [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, + + // Library management + [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, + [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, + [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, + [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, +}; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/library.repository.ts b/server/src/repositories/library.repository.ts similarity index 91% rename from server/src/infra/repositories/library.repository.ts rename to server/src/repositories/library.repository.ts index 5ae3de96f..5cfa25eff 100644 --- a/server/src/infra/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -1,11 +1,12 @@ -import { ILibraryRepository, LibraryStatsResponseDto } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Not } from 'typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { EntityNotFoundError, IsNull, Not } from 'typeorm'; import { Repository } from 'typeorm/repository/Repository.js'; -import { LibraryEntity, LibraryType } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() @@ -138,6 +139,10 @@ export class LibraryRepository implements ILibraryRepository { .where('libraries.id = :id', { id }) .getRawOne(); + if (!stats) { + throw new EntityNotFoundError(LibraryEntity, { where: { id } }); + } + return { photos: Number(stats.photos), videos: Number(stats.videos), diff --git a/server/src/infra/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts similarity index 92% rename from server/src/infra/repositories/machine-learning.repository.ts rename to server/src/repositories/machine-learning.repository.ts index 767ca812b..3d8f0cac1 100644 --- a/server/src/infra/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,17 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { readFile } from 'node:fs/promises'; +import { CLIPConfig, ModelConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; import { - CLIPConfig, CLIPMode, DetectFaceResult, IMachineLearningRepository, - ModelConfig, ModelType, - RecognitionConfig, TextModelInput, VisionModelInput, -} from '@app/domain'; -import { Injectable } from '@nestjs/common'; -import { readFile } from 'node:fs/promises'; -import { Instrumentation } from '../instrumentation'; +} from 'src/interfaces/machine-learning.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; const errorPrefix = 'Machine learning request'; diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/repositories/media.repository.ts similarity index 95% rename from server/src/infra/repositories/media.repository.ts rename to server/src/repositories/media.repository.ts index 39cec03af..52a538909 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,19 +1,19 @@ +import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; +import fs from 'node:fs/promises'; +import { Writable } from 'node:stream'; +import { promisify } from 'node:util'; +import sharp from 'sharp'; +import { Colorspace } from 'src/entities/system-config.entity'; import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo, - handlePromiseError, -} from '@app/domain'; -import { Colorspace } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; -import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; -import fs from 'node:fs/promises'; -import { Writable } from 'node:stream'; -import { promisify } from 'node:util'; -import sharp from 'sharp'; -import { Instrumentation } from '../instrumentation'; +} from 'src/interfaces/media.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { handlePromiseError } from 'src/utils/misc'; const probe = promisify(ffmpeg.ffprobe); sharp.concurrency(0); diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts new file mode 100644 index 000000000..ae8346d00 --- /dev/null +++ b/server/src/repositories/memory.repository.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { DataSource, In, Repository } from 'typeorm'; + +@Instrumentation() +@Injectable() +export class MemoryRepository implements IMemoryRepository { + constructor( + @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(MemoryEntity) private repository: Repository, + @InjectDataSource() private dataSource: DataSource, + ) {} + + search(ownerId: string): Promise { + return this.repository.find({ + where: { + ownerId, + }, + order: { + memoryAt: 'DESC', + }, + }); + } + + get(id: string): Promise { + return this.repository.findOne({ + where: { + id, + }, + relations: { + assets: true, + }, + }); + } + + create(memory: Partial): Promise { + return this.save(memory); + } + + update(memory: Partial): Promise { + return this.save(memory); + } + + async delete(id: string): Promise { + await this.repository.delete({ id }); + } + + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @ChunkedSet({ paramIndex: 1 }) + async getAssetIds(id: string, assetIds: string[]): Promise> { + if (assetIds.length === 0) { + return new Set(); + } + + const results = await this.dataSource + .createQueryBuilder() + .select('memories_assets.assetsId', 'assetId') + .from('memories_assets_assets', 'memories_assets') + .where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) + .andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) + .getRawMany(); + + return new Set(results.map((row) => row['assetId'])); + } + + @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] }) + async addAssetIds(id: string, assetIds: string[]): Promise { + await this.dataSource + .createQueryBuilder() + .insert() + .into('memories_assets_assets', ['memoriesId', 'assetsId']) + .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) + async removeAssetIds(id: string, assetIds: string[]): Promise { + await this.dataSource + .createQueryBuilder() + .delete() + .from('memories_assets_assets') + .where({ + memoriesId: id, + assetsId: In(assetIds), + }) + .execute(); + } + + private async save(memory: Partial): Promise { + const { id } = await this.repository.save(memory); + return this.repository.findOneOrFail({ + where: { id }, + relations: { + assets: true, + }, + }); + } +} diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts similarity index 92% rename from server/src/infra/repositories/metadata.repository.ts rename to server/src/repositories/metadata.repository.ts index bf9bb8a46..511023a8e 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -1,29 +1,22 @@ -import { - citiesFile, - geodataAdmin1Path, - geodataAdmin2Path, - geodataCities500Path, - geodataDatePath, - GeoPoint, - IMetadataRepository, - ImmichTags, - ISystemMetadataRepository, - ReverseGeocodeResult, -} from '@app/domain'; -import { ExifEntity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { Inject } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; -import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored'; -import * as geotz from 'geo-tz'; +import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored'; +import geotz from 'geo-tz'; import { getName } from 'i18n-iso-countries'; import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import * as readLine from 'node:readline'; +import readLine from 'node:readline'; +import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; import { DataSource, QueryRunner, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() export class MetadataRepository implements IMetadataRepository { diff --git a/server/src/repositories/metric.repository.ts b/server/src/repositories/metric.repository.ts new file mode 100644 index 000000000..c6eb953ac --- /dev/null +++ b/server/src/repositories/metric.repository.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { MetricOptions } from '@opentelemetry/api'; +import { MetricService } from 'nestjs-otel'; +import { IMetricGroupRepository, IMetricRepository, MetricGroupOptions } from 'src/interfaces/metric.interface'; +import { apiMetrics, hostMetrics, jobMetrics, repoMetrics } from 'src/utils/instrumentation'; + +class MetricGroupRepository implements IMetricGroupRepository { + private enabled = false; + constructor(private readonly metricService: MetricService) {} + + addToCounter(name: string, value: number, options?: MetricOptions): void { + if (this.enabled) { + this.metricService.getCounter(name, options).add(value); + } + } + + addToGauge(name: string, value: number, options?: MetricOptions): void { + if (this.enabled) { + this.metricService.getUpDownCounter(name, options).add(value); + } + } + + addToHistogram(name: string, value: number, options?: MetricOptions): void { + if (this.enabled) { + this.metricService.getHistogram(name, options).record(value); + } + } + + configure(options: MetricGroupOptions): this { + this.enabled = options.enabled; + return this; + } +} + +@Injectable() +export class MetricRepository implements IMetricRepository { + api: MetricGroupRepository; + host: MetricGroupRepository; + jobs: MetricGroupRepository; + repo: MetricGroupRepository; + + constructor(metricService: MetricService) { + this.api = new MetricGroupRepository(metricService).configure({ enabled: apiMetrics }); + this.host = new MetricGroupRepository(metricService).configure({ enabled: hostMetrics }); + this.jobs = new MetricGroupRepository(metricService).configure({ enabled: jobMetrics }); + this.repo = new MetricGroupRepository(metricService).configure({ enabled: repoMetrics }); + } +} diff --git a/server/src/infra/repositories/move.repository.ts b/server/src/repositories/move.repository.ts similarity index 76% rename from server/src/infra/repositories/move.repository.ts rename to server/src/repositories/move.repository.ts index 205c67ad6..a8416ff0a 100644 --- a/server/src/infra/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -1,10 +1,10 @@ -import { IMoveRepository, MoveCreate } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { MoveEntity, PathType } from 'src/entities/move.entity'; +import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { MoveEntity, PathType } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts similarity index 86% rename from server/src/infra/repositories/partner.repository.ts rename to server/src/repositories/partner.repository.ts index eb07902dc..8465493b5 100644 --- a/server/src/infra/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -1,9 +1,9 @@ -import { IPartnerRepository, PartnerIds } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { PartnerEntity } from 'src/entities/partner.entity'; +import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { DeepPartial, Repository } from 'typeorm'; -import { PartnerEntity } from '../entities'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/repositories/person.repository.ts similarity index 94% rename from server/src/infra/repositories/person.repository.ts rename to server/src/repositories/person.repository.ts index 562a56fb6..0acbafe69 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -1,21 +1,22 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import _ from 'lodash'; +import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { PersonEntity } from 'src/entities/person.entity'; import { AssetFaceId, IPersonRepository, - Paginated, - PaginationOptions, PeopleStatistics, PersonNameSearchOptions, PersonSearchOptions, PersonStatistics, UpdateFacesData, -} from '@app/domain'; -import { InjectRepository } from '@nestjs/typeorm'; -import _ from 'lodash'; +} from 'src/interfaces/person.interface'; +import { asVector } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { Paginated, PaginationOptions, paginate } from 'src/utils/pagination'; import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm'; -import { AssetEntity, AssetFaceEntity, PersonEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { ChunkedArray, asVector, paginate } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() export class PersonRepository implements IPersonRepository { @@ -106,6 +107,7 @@ export class PersonRepository implements IPersonRepository { @GenerateSql({ params: [DummyValue.UUID] }) getFaceById(id: string): Promise { + // TODO return null instead of find or fail return this.assetFaceRepository.findOneOrFail({ where: { id }, relations: { diff --git a/server/src/infra/repositories/search.repository.ts b/server/src/repositories/search.repository.ts similarity index 76% rename from server/src/infra/repositories/search.repository.ts rename to server/src/repositories/search.repository.ts index f5d1cbda3..4530d2295 100644 --- a/server/src/infra/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -1,38 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { vectorExt } from 'src/database.config'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { SmartSearchEntity } from 'src/entities/smart-search.entity'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { AssetSearchOptions, - DatabaseExtension, - Embedding, FaceEmbeddingSearch, FaceSearchResult, ISearchRepository, - Paginated, - PaginationMode, - PaginationResult, SearchPaginationOptions, SmartSearchOptions, -} from '@app/domain'; -import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; -import { - AssetEntity, - AssetFaceEntity, - GeodataPlacesEntity, - SmartInfoEntity, - SmartSearchEntity, -} from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +} from 'src/interfaces/search.interface'; +import { asVector, searchAssetBuilder } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { getCLIPModelInfo } from 'src/utils/misc'; +import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; +import { isValidInteger } from 'src/validation'; import { Repository, SelectQueryBuilder } from 'typeorm'; -import { vectorExt } from '../database.config'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() export class SearchRepository implements ISearchRepository { private logger = new ImmichLogger(SearchRepository.name); private faceColumns: string[]; + private assetsByCityQuery: string; constructor( @InjectRepository(SmartInfoEntity) private repository: Repository, @@ -45,6 +42,14 @@ export class SearchRepository implements ISearchRepository { .getMetadata(AssetFaceEntity) .ownColumns.map((column) => column.propertyName) .filter((propertyName) => propertyName !== 'embedding'); + this.assetsByCityQuery = + assetsByCityCte + + this.assetRepository + .createQueryBuilder('asset') + .innerJoinAndSelect('asset.exifInfo', 'exif') + .withDeleted() + .getQuery() + + ' INNER JOIN cte ON asset.id = cte."assetId"'; } async init(modelName: string): Promise { @@ -209,10 +214,10 @@ export class SearchRepository implements ISearchRepository { .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`) .orderBy( ` - COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0) + COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0.1) `, ) .setParameters({ placeName }) @@ -220,16 +225,28 @@ export class SearchRepository implements ISearchRepository { .getMany(); } - async upsert(smartInfo: Partial, embedding?: Embedding): Promise { - await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] }); - if (!smartInfo.assetId || !embedding) { - return; + @GenerateSql({ params: [[DummyValue.UUID]] }) + async getAssetsByCity(userIds: string[]): Promise { + const parameters = [userIds, true, false, AssetType.IMAGE]; + const rawRes = await this.repository.query(this.assetsByCityQuery, parameters); + + const items: AssetEntity[] = []; + for (const res of rawRes) { + const item = { exifInfo: {} as Record } as Record; + for (const [key, value] of Object.entries(res)) { + if (key.startsWith('exif_')) { + item.exifInfo[key.replace('exif_', '')] = value; + } else { + item[key.replace('asset_', '')] = value; + } + } + items.push(item as AssetEntity); } - await this.upsertEmbedding(smartInfo.assetId, embedding); + return items; } - private async upsertEmbedding(assetId: string, embedding: number[]): Promise { + async upsert(assetId: string, embedding: number[]): Promise { await this.smartSearchRepository.upsert( { assetId, embedding: () => asVector(embedding, true) }, { conflictPaths: ['assetId'] }, @@ -290,3 +307,30 @@ export class SearchRepository implements ISearchRepository { return runtimeConfig; } } + +// the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms +const assetsByCityCte = ` +WITH RECURSIVE cte AS ( + ( + SELECT city, "assetId" + FROM exif + INNER JOIN assets ON exif."assetId" = assets.id + WHERE "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4 + ORDER BY city + LIMIT 1 + ) + + UNION ALL + + SELECT l.city, l."assetId" + FROM cte c + , LATERAL ( + SELECT city, "assetId" + FROM exif + INNER JOIN assets ON exif."assetId" = assets.id + WHERE city > c.city AND "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4 + ORDER BY city + LIMIT 1 + ) l +) +`; diff --git a/server/src/infra/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts similarity index 79% rename from server/src/infra/repositories/server-info.repository.ts rename to server/src/repositories/server-info.repository.ts index bd56a58dd..5f14a881c 100644 --- a/server/src/infra/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -1,6 +1,6 @@ -import { GitHubRelease, IServerInfoRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; -import { Instrumentation } from '../instrumentation'; +import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts similarity index 88% rename from server/src/infra/repositories/shared-link.repository.ts rename to server/src/repositories/shared-link.repository.ts index 5e796a772..48dbb3ab9 100644 --- a/server/src/infra/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -1,10 +1,10 @@ -import { ISharedLinkRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { SharedLinkEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/filesystem.provider.spec.ts b/server/src/repositories/storage.repository.spec.ts similarity index 95% rename from server/src/infra/repositories/filesystem.provider.spec.ts rename to server/src/repositories/storage.repository.spec.ts index 4c20b2a50..b92a26904 100644 --- a/server/src/infra/repositories/filesystem.provider.spec.ts +++ b/server/src/repositories/storage.repository.spec.ts @@ -1,6 +1,6 @@ -import { CrawlOptionsDto } from '@app/domain'; import mockfs from 'mock-fs'; -import { FilesystemProvider } from './filesystem.provider'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { StorageRepository } from 'src/repositories/storage.repository'; interface Test { test: string; @@ -179,11 +179,11 @@ const tests: Test[] = [ }, ]; -describe(FilesystemProvider.name, () => { - let sut: FilesystemProvider; +describe(StorageRepository.name, () => { + let sut: StorageRepository; beforeEach(() => { - sut = new FilesystemProvider(); + sut = new StorageRepository(); }); afterEach(() => { diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/repositories/storage.repository.ts similarity index 93% rename from server/src/infra/repositories/filesystem.provider.ts rename to server/src/repositories/storage.repository.ts index c4f577ed2..7e1c8d59e 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/repositories/storage.repository.ts @@ -1,25 +1,25 @@ -import { - CrawlOptionsDto, - DiskUsage, - IStorageRepository, - ImmichReadStream, - ImmichZipStream, - StorageEventType, - WatchEvents, - mimeTypes, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; import chokidar, { WatchOptions } from 'chokidar'; import { glob, globStream } from 'fast-glob'; import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { Instrumentation } from '../instrumentation'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { + DiskUsage, + IStorageRepository, + ImmichReadStream, + ImmichZipStream, + StorageEventType, + WatchEvents, +} from 'src/interfaces/storage.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; @Instrumentation() -export class FilesystemProvider implements IStorageRepository { - private logger = new ImmichLogger(FilesystemProvider.name); +export class StorageRepository implements IStorageRepository { + private logger = new ImmichLogger(StorageRepository.name); readdir(folder: string): Promise { return fs.readdir(folder); diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/repositories/system-config.repository.ts similarity index 81% rename from server/src/infra/repositories/system-config.repository.ts rename to server/src/repositories/system-config.repository.ts index 598333d9f..baa3218b0 100644 --- a/server/src/infra/repositories/system-config.repository.ts +++ b/server/src/repositories/system-config.repository.ts @@ -1,11 +1,10 @@ -import { ISystemConfigRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import { readFile } from 'node:fs/promises'; +import { Chunked, DummyValue, GenerateSql } from 'src/decorators'; +import { SystemConfigEntity } from 'src/entities/system-config.entity'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { In, Repository } from 'typeorm'; -import { SystemConfigEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Chunked } from '../infra.utils'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() export class SystemConfigRepository implements ISystemConfigRepository { diff --git a/server/src/infra/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts similarity index 76% rename from server/src/infra/repositories/system-metadata.repository.ts rename to server/src/repositories/system-metadata.repository.ts index 5b99cd1dd..80936e46f 100644 --- a/server/src/infra/repositories/system-metadata.repository.ts +++ b/server/src/repositories/system-metadata.repository.ts @@ -1,8 +1,8 @@ -import { ISystemMetadataRepository } from '@app/domain/repositories/system-metadata.repository'; import { InjectRepository } from '@nestjs/typeorm'; +import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { SystemMetadata, SystemMetadataEntity } from '../entities'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() export class SystemMetadataRepository implements ISystemMetadataRepository { diff --git a/server/src/infra/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts similarity index 92% rename from server/src/infra/repositories/tag.repository.ts rename to server/src/repositories/tag.repository.ts index 3ac5afd0e..6fa827906 100644 --- a/server/src/infra/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,9 +1,10 @@ -import { ITagRepository } from '@app/domain'; -import { AssetEntity, TagEntity } from '@app/infra/entities'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { TagEntity } from 'src/entities/tag.entity'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/user-token.repository.ts b/server/src/repositories/user-token.repository.ts similarity index 82% rename from server/src/infra/repositories/user-token.repository.ts rename to server/src/repositories/user-token.repository.ts index 60eccb2e5..cbf3a3e3b 100644 --- a/server/src/infra/repositories/user-token.repository.ts +++ b/server/src/repositories/user-token.repository.ts @@ -1,10 +1,10 @@ -import { IUserTokenRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; -import { UserTokenEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/repositories/user.repository.ts similarity index 92% rename from server/src/infra/repositories/user.repository.ts rename to server/src/repositories/user.repository.ts index 865a9c8cb..f0e00d049 100644 --- a/server/src/infra/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -1,10 +1,16 @@ -import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryResponse } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { + IUserRepository, + UserFindOptions, + UserListFilter, + UserStatsQueryResponse, +} from 'src/interfaces/user.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Not, Repository } from 'typeorm'; -import { AssetEntity, UserEntity } from '../entities'; -import { DummyValue, GenerateSql } from '../infra.util'; -import { Instrumentation } from '../instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/services/activity.service.spec.ts similarity index 92% rename from server/src/domain/activity/activity.spec.ts rename to server/src/services/activity.service.spec.ts index 10a4c0725..e7049ea6c 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -1,10 +1,11 @@ import { BadRequestException } from '@nestjs/common'; -import { authStub, IAccessRepositoryMock, newAccessRepositoryMock } from '@test'; -import { activityStub } from '@test/fixtures/activity.stub'; -import { newActivityRepositoryMock } from '@test/repositories/activity.repository.mock'; -import { IActivityRepository } from '../repositories'; -import { ReactionType } from './activity.dto'; -import { ActivityService } from './activity.service'; +import { ReactionType } from 'src/dtos/activity.dto'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { ActivityService } from 'src/services/activity.service'; +import { activityStub } from 'test/fixtures/activity.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; describe(ActivityService.name, () => { let sut: ActivityService; diff --git a/server/src/domain/activity/activity.service.ts b/server/src/services/activity.service.ts similarity index 87% rename from server/src/domain/activity/activity.service.ts rename to server/src/services/activity.service.ts index 69386f561..7589fb8cc 100644 --- a/server/src/domain/activity/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,8 +1,5 @@ -import { ActivityEntity } from '@app/infra/entities'; import { Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from '../access'; -import { AuthDto } from '../auth'; -import { IAccessRepository, IActivityRepository } from '../repositories'; +import { AccessCore, Permission } from 'src/cores/access.core'; import { ActivityCreateDto, ActivityDto, @@ -13,7 +10,11 @@ import { ReactionLevel, ReactionType, mapActivity, -} from './activity.dto'; +} from 'src/dtos/activity.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; @Injectable() export class ActivityService { diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/services/album.service.spec.ts similarity index 93% rename from server/src/domain/album/album.service.spec.ts rename to server/src/services/album.service.spec.ts index fa0852d8c..02bb607b5 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -1,36 +1,32 @@ import { BadRequestException } from '@nestjs/common'; -import { - albumStub, - authStub, - IAccessRepositoryMock, - newAccessRepositoryMock, - newAlbumRepositoryMock, - newAssetRepositoryMock, - newJobRepositoryMock, - newUserRepositoryMock, - userStub, -} from '@test'; import _ from 'lodash'; -import { BulkIdErrorReason } from '../asset'; -import { IAlbumRepository, IAssetRepository, IJobRepository, IUserRepository } from '../repositories'; -import { AlbumService } from './album.service'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AlbumService } from 'src/services/album.service'; +import { albumStub } from 'test/fixtures/album.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; describe(AlbumService.name, () => { let sut: AlbumService; let accessMock: IAccessRepositoryMock; let albumMock: jest.Mocked; let assetMock: jest.Mocked; - let jobMock: jest.Mocked; let userMock: jest.Mocked; beforeEach(() => { accessMock = newAccessRepositoryMock(); albumMock = newAlbumRepositoryMock(); assetMock = newAssetRepositoryMock(); - jobMock = newJobRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new AlbumService(accessMock, albumMock, assetMock, jobMock, userMock); + sut = new AlbumService(accessMock, albumMock, assetMock, userMock); }); it('should work', () => { @@ -522,10 +518,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', }); - expect(albumMock.addAssets).toHaveBeenCalledWith({ - albumId: 'album-123', - assetIds: ['asset-1', 'asset-2', 'asset-3'], - }); + expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); }); it('should not set the thumbnail if the album has one already', async () => { @@ -543,7 +536,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-id', }); - expect(albumMock.addAssets).toHaveBeenCalled(); + expect(albumMock.addAssetIds).toHaveBeenCalled(); }); it('should allow a shared user to add assets', async () => { @@ -565,10 +558,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', }); - expect(albumMock.addAssets).toHaveBeenCalledWith({ - albumId: 'album-123', - assetIds: ['asset-1', 'asset-2', 'asset-3'], - }); + expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); }); it('should allow a shared link user to add assets', async () => { @@ -590,10 +580,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', }); - expect(albumMock.addAssets).toHaveBeenCalledWith({ - albumId: 'album-123', - assetIds: ['asset-1', 'asset-2', 'asset-3'], - }); + expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLink?.id, @@ -669,23 +656,23 @@ describe(AlbumService.name, () => { describe('removeAssets', () => { it('should allow the owner to remove assets', async () => { - accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); - accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); - albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); + albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id'])); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: true, id: 'asset-id' }, ]); expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) }); - expect(albumMock.removeAssets).toHaveBeenCalledWith('album-123', ['asset-id']); + expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']); }); it('should skip assets not in the album', async () => { accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty)); - albumMock.getAssetIds.mockResolvedValueOnce(new Set()); + albumMock.getAssetIds.mockResolvedValue(new Set()); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND }, @@ -697,7 +684,7 @@ describe(AlbumService.name, () => { it('should skip assets without user permission to remove', async () => { accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); - albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); + albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id'])); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { @@ -711,10 +698,10 @@ describe(AlbumService.name, () => { }); it('should reset the thumbnail if it is removed', async () => { - accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); - accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); - albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); + albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id'])); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: true, id: 'asset-id' }, diff --git a/server/src/domain/album/album.service.ts b/server/src/services/album.service.ts similarity index 75% rename from server/src/domain/album/album.service.ts rename to server/src/services/album.service.ts index dc3d510d4..df6c6b814 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,35 +1,35 @@ -import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from '../access'; -import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset'; -import { AuthDto } from '../auth'; -import { setUnion } from '../domain.util'; -import { - AlbumAssetCount, - AlbumInfoOptions, - IAccessRepository, - IAlbumRepository, - IAssetRepository, - IJobRepository, - IUserRepository, -} from '../repositories'; +import { AccessCore, Permission } from 'src/cores/access.core'; import { + AddUsersDto, AlbumCountResponseDto, + AlbumInfoDto, AlbumResponseDto, + CreateAlbumDto, + GetAlbumsDto, + UpdateAlbumDto, mapAlbum, mapAlbumWithAssets, mapAlbumWithoutAssets, -} from './album-response.dto'; -import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto'; +} from 'src/dtos/album.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class AlbumService { private access: AccessCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private accessRepository: IAccessRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IUserRepository) private userRepository: IUserRepository, ) { this.access = AccessCore.create(accessRepository); @@ -164,37 +164,20 @@ export class AlbumService { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(auth, Permission.ALBUM_READ, id); - const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); - const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id)); - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const results = await addAssets( + auth, + { accessRepository: this.accessRepository, repository: this.albumRepository }, + { id, assetIds: dto.ids }, + ); - const results: BulkIdResponseDto[] = []; - for (const assetId of dto.ids) { - const hasAsset = existingAssetIds.has(assetId); - if (hasAsset) { - results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE }); - continue; - } - - const hasAccess = allowedAssetIds.has(assetId); - if (!hasAccess) { - results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); - continue; - } - - results.push({ id: assetId, success: true }); - } - - const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); - if (newAssetIds.length > 0) { - await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds }); + const { id: firstNewAssetId } = results.find(({ success }) => success) || {}; + if (firstNewAssetId) { await this.albumRepository.update({ id, updatedAt: new Date(), - albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0], + albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); } @@ -206,31 +189,14 @@ export class AlbumService { await this.access.requirePermission(auth, Permission.ALBUM_READ, id); - const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); - const canRemove = await this.access.checkAccess(auth, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); - const canShare = await this.access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); - const allowedAssetIds = setUnion(canRemove, canShare); - - const results: BulkIdResponseDto[] = []; - for (const assetId of dto.ids) { - const hasAsset = existingAssetIds.has(assetId); - if (!hasAsset) { - results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND }); - continue; - } - - const hasAccess = allowedAssetIds.has(assetId); - if (!hasAccess) { - results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); - continue; - } - - results.push({ id: assetId, success: true }); - } + const results = await removeAssets( + auth, + { accessRepository: this.accessRepository, repository: this.albumRepository }, + { id, assetIds: dto.ids, permissions: [Permission.ASSET_SHARE, Permission.ALBUM_REMOVE_ASSET] }, + ); const removedIds = results.filter(({ success }) => success).map(({ id }) => id); if (removedIds.length > 0) { - await this.albumRepository.removeAssets(id, removedIds); await this.albumRepository.update({ id, updatedAt: new Date() }); if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) { await this.albumRepository.updateThumbnails(); diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts similarity index 85% rename from server/src/domain/api-key/api-key.service.spec.ts rename to server/src/services/api-key.service.spec.ts index f3b291084..47fd0f515 100644 --- a/server/src/domain/api-key/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -1,7 +1,11 @@ import { BadRequestException } from '@nestjs/common'; -import { authStub, keyStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '@test'; -import { ICryptoRepository, IKeyRepository } from '../repositories'; -import { APIKeyService } from './api-key.service'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { APIKeyService } from 'src/services/api-key.service'; +import { keyStub } from 'test/fixtures/api-key.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; describe(APIKeyService.name, () => { let sut: APIKeyService; @@ -23,7 +27,7 @@ describe(APIKeyService.name, () => { name: 'Test Key', userId: authStub.admin.user.id, }); - expect(cryptoMock.randomBytes).toHaveBeenCalled(); + expect(cryptoMock.newPassword).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled(); }); @@ -37,7 +41,7 @@ describe(APIKeyService.name, () => { name: 'API Key', userId: authStub.admin.user.id, }); - expect(cryptoMock.randomBytes).toHaveBeenCalled(); + expect(cryptoMock.newPassword).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled(); }); }); diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/services/api-key.service.ts similarity index 85% rename from server/src/domain/api-key/api-key.service.ts rename to server/src/services/api-key.service.ts index 0eef1981c..24a57d365 100644 --- a/server/src/domain/api-key/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,8 +1,9 @@ -import { APIKeyEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AuthDto } from '../auth'; -import { ICryptoRepository, IKeyRepository } from '../repositories'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @Injectable() export class APIKeyService { @@ -12,7 +13,7 @@ export class APIKeyService { ) {} async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { - const secret = this.crypto.randomBytes(32).toString('base64').replaceAll(/\W/g, ''); + const secret = this.crypto.newPassword(32); const entity = await this.repository.create({ key: this.crypto.hashSha256(secret), name: dto.name || 'API Key', diff --git a/server/src/immich/app.service.ts b/server/src/services/api.service.ts similarity index 82% rename from server/src/immich/app.service.ts rename to server/src/services/api.service.ts index adfb9d878..87107b23f 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/services/api.service.ts @@ -1,21 +1,18 @@ -import { - AuthService, - DatabaseService, - JobService, - ONE_HOUR, - OpenGraphTags, - ServerInfoService, - SharedLinkService, - StorageService, - SystemConfigService, - WEB_ROOT, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; import { Injectable } from '@nestjs/common'; import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +import { ONE_HOUR, WEB_ROOT } from 'src/constants'; +import { AuthService } from 'src/services/auth.service'; +import { DatabaseService } from 'src/services/database.service'; +import { JobService } from 'src/services/job.service'; +import { ServerInfoService } from 'src/services/server-info.service'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import { StorageService } from 'src/services/storage.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { ImmichLogger } from 'src/utils/logger'; +import { OpenGraphTags } from 'src/utils/misc'; const render = (index: string, meta: OpenGraphTags) => { const tags = ` @@ -38,8 +35,8 @@ const render = (index: string, meta: OpenGraphTags) => { }; @Injectable() -export class AppService { - private logger = new ImmichLogger(AppService.name); +export class ApiService { + private logger = new ImmichLogger(ApiService.name); constructor( private authService: AuthService, diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/services/asset-v1.service.spec.ts similarity index 81% rename from server/src/immich/api-v1/asset/asset.service.spec.ts rename to server/src/services/asset-v1.service.spec.ts index 9f0aa371e..735ac8322 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/services/asset-v1.service.spec.ts @@ -1,30 +1,25 @@ -import { - IAssetRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - IUserRepository, - JobName, -} from '@app/domain'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; -import { - IAccessRepositoryMock, - assetStub, - authStub, - fileStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newJobRepositoryMock, - newLibraryRepositoryMock, - newStorageRepositoryMock, - newUserRepositoryMock, -} from '@test'; import { when } from 'jest-when'; +import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto'; +import { CreateAssetDto } from 'src/dtos/asset-v1.dto'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AssetServiceV1 } from 'src/services/asset-v1.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { fileStub } from 'test/fixtures/file.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { QueryFailedError } from 'typeorm'; -import { IAssetRepositoryV1 } from './asset-repository'; -import { AssetService } from './asset.service'; -import { CreateAssetDto } from './dto/create-asset.dto'; -import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto'; const _getCreateAssetDto = (): CreateAssetDto => { const createAssetDto = new CreateAssetDto(); @@ -49,13 +44,13 @@ const _getAsset_1 = () => { asset_1.deviceId = 'device_id_1'; asset_1.type = AssetType.VIDEO; asset_1.originalPath = 'fake_path/asset_1.jpeg'; - asset_1.resizePath = ''; + asset_1.previewPath = ''; asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.isFavorite = false; asset_1.isArchived = false; - asset_1.webpPath = ''; + asset_1.thumbnailPath = ''; asset_1.encodedVideoPath = ''; asset_1.duration = '0:00:00.000000'; asset_1.exifInfo = new ExifEntity(); @@ -65,7 +60,7 @@ const _getAsset_1 = () => { }; describe('AssetService', () => { - let sut: AssetService; + let sut: AssetServiceV1; let accessMock: IAccessRepositoryMock; let assetRepositoryMockV1: jest.Mocked; let assetMock: jest.Mocked; @@ -93,7 +88,7 @@ describe('AssetService', () => { storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock); + sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock); when(assetRepositoryMockV1.get) .calledWith(assetStub.livePhotoStillAsset.id) diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/services/asset-v1.service.ts similarity index 84% rename from server/src/immich/api-v1/asset/asset.service.ts rename to server/src/services/asset-v1.service.ts index 821a7de82..97aa99d91 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/services/asset-v1.service.ts @@ -1,24 +1,3 @@ -import { - AccessCore, - AssetResponseDto, - AuthDto, - CacheControl, - IAccessRepository, - IAssetRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - IUserRepository, - ImmichFileResponse, - JobName, - Permission, - UploadFile, - getLivePhotoMotionFilename, - mapAsset, - mimeTypes, -} from '@app/domain'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, @@ -26,27 +5,46 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { QueryFailedError } from 'typeorm'; -import { IAssetRepositoryV1 } from './asset-repository'; -import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; -import { AssetSearchDto } from './dto/asset-search.dto'; -import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetDto } from './dto/create-asset.dto'; -import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; -import { ServeFileDto } from './dto/serve-file.dto'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetBulkUploadCheckResponseDto, + AssetFileUploadResponseDto, AssetRejectReason, AssetUploadAction, -} from './response-dto/asset-check-response.dto'; -import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; -import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; -import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; + CheckExistingAssetsResponseDto, + CuratedLocationsResponseDto, + CuratedObjectsResponseDto, +} from 'src/dtos/asset-v1-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetSearchDto, + CheckExistingAssetsDto, + CreateAssetDto, + GetAssetThumbnailDto, + GetAssetThumbnailFormatEnum, + ServeFileDto, +} from 'src/dtos/asset-v1.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { LibraryType } from 'src/entities/library.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { UploadFile } from 'src/services/asset.service'; +import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { QueryFailedError } from 'typeorm'; @Injectable() -export class AssetService { - readonly logger = new ImmichLogger(AssetService.name); +/** @deprecated */ +export class AssetServiceV1 { + readonly logger = new ImmichLogger(AssetServiceV1.name); private access: AccessCore; constructor( @@ -249,16 +247,16 @@ export class AssetService { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: { - if (asset.webpPath) { - return asset.webpPath; + if (asset.thumbnailPath) { + return asset.thumbnailPath; } this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); } case GetAssetThumbnailFormatEnum.JPEG: { - if (!asset.resizePath) { + if (!asset.previewPath) { throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); } - return asset.resizePath; + return asset.previewPath; } } } @@ -270,12 +268,12 @@ export class AssetService { * Serve file viewer on the web */ if (dto.isWeb && mimeType != 'image/gif') { - if (!asset.resizePath) { + if (!asset.previewPath) { this.logger.error('Error serving IMAGE asset for web'); throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); } - return asset.resizePath; + return asset.previewPath; } /** @@ -285,15 +283,15 @@ export class AssetService { return asset.originalPath; } - if (asset.webpPath && asset.webpPath.length > 0) { - return asset.webpPath; + if (asset.thumbnailPath && asset.thumbnailPath.length > 0) { + return asset.thumbnailPath; } - if (!asset.resizePath) { - throw new Error('resizePath not set'); + if (!asset.previewPath) { + throw new Error('previewPath not set'); } - return asset.resizePath; + return asset.previewPath; } private async getLibraryId(auth: AuthDto, libraryId?: string) { diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/services/asset.service.spec.ts old mode 100644 new mode 100755 similarity index 81% rename from server/src/domain/asset/asset.service.spec.ts rename to server/src/services/asset.service.spec.ts index 361946f61..b90c194aa --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,42 +1,31 @@ -import { AssetEntity, AssetType } from '@app/infra/entities'; import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - assetStackStub, - assetStub, - authStub, - faceStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newAssetStackRepositoryMock, - newCommunicationRepositoryMock, - newJobRepositoryMock, - newPartnerRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - newUserRepositoryMock, - partnerStub, - userStub, -} from '@test'; import { when } from 'jest-when'; -import { JobName } from '../job'; -import { - AssetStats, - ClientEvent, - IAssetRepository, - IAssetStackRepository, - ICommunicationRepository, - IJobRepository, - IPartnerRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, - JobItem, - TimeBucketSize, -} from '../repositories'; -import { AssetService, UploadFieldName } from './asset.service'; -import { AssetJobName, AssetStatsResponseDto } from './dto'; -import { mapAsset } from './response-dto'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AssetService } from 'src/services/asset.service'; +import { assetStackStub, assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { faceStub } from 'test/fixtures/face.stub'; +import { partnerStub } from 'test/fixtures/partner.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; const stats: AssetStats = { [AssetType.IMAGE]: 10, @@ -163,7 +152,7 @@ describe(AssetService.name, () => { let jobMock: jest.Mocked; let storageMock: jest.Mocked; let userMock: jest.Mocked; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; let configMock: jest.Mocked; let partnerMock: jest.Mocked; let assetStackMock: jest.Mocked; @@ -175,7 +164,7 @@ describe(AssetService.name, () => { beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); + eventMock = newEventRepositoryMock(); jobMock = newJobRepositoryMock(); storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); @@ -190,7 +179,7 @@ describe(AssetService.name, () => { configMock, storageMock, userMock, - communicationMock, + eventMock, partnerMock, assetStackMock, ); @@ -318,13 +307,19 @@ describe(AssetService.name, () => { jest.useRealTimers(); }); - it('should set the title correctly', async () => { + it('should group the assets correctly', async () => { + const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) }; + const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) }; + const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) }; + const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) }; + partnerMock.getAll.mockResolvedValue([]); - assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]); + assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3, image4]); await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ - { title: '1 year since...', assets: [mapAsset(assetStub.image)] }, - { title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, + { yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] }, + { yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] }, + { yearsAgo: 15, title: '15 years since...', assets: [mapAsset(image4)] }, ]); expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]); @@ -332,6 +327,7 @@ describe(AssetService.name, () => { it('should get memories with partners with inTimeline enabled', async () => { partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]); + assetMock.getByDayOfYear.mockResolvedValue([]); await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 }); @@ -341,129 +337,6 @@ describe(AssetService.name, () => { }); }); - describe('getTimeBuckets', () => { - it("should return buckets if userId and albumId aren't set", async () => { - assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]); - - await expect( - sut.getTimeBuckets(authStub.admin, { - size: TimeBucketSize.DAY, - }), - ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); - expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id] }); - }); - }); - - describe('getTimeBucket', () => { - it('should return the assets for a album time bucket if user has album.read', async () => { - accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); - assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); - - await expect( - sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - - expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id'])); - expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - albumId: 'album-id', - }); - }); - - it('should return the assets for a archive time bucket if user has archive.read', async () => { - assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); - - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isArchived: true, - userId: authStub.admin.user.id, - }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isArchived: true, - userIds: [authStub.admin.user.id], - }); - }); - - it('should return the assets for a library time bucket if user has library.read', async () => { - assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); - - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - userId: authStub.admin.user.id, - }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - userIds: [authStub.admin.user.id], - }); - }); - - it('should throw an error if withParners is true and isArchived true or undefined', async () => { - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isArchived: true, - withPartners: true, - userId: authStub.admin.user.id, - }), - ).rejects.toThrowError(BadRequestException); - - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isArchived: undefined, - withPartners: true, - userId: authStub.admin.user.id, - }), - ).rejects.toThrowError(BadRequestException); - }); - - it('should throw an error if withParners is true and isFavorite is either true or false', async () => { - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isFavorite: true, - withPartners: true, - userId: authStub.admin.user.id, - }), - ).rejects.toThrowError(BadRequestException); - - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isFavorite: false, - withPartners: true, - userId: authStub.admin.user.id, - }), - ).rejects.toThrowError(BadRequestException); - }); - - it('should throw an error if withParners is true and isTrash is true', async () => { - await expect( - sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - isTrashed: true, - withPartners: true, - userId: authStub.admin.user.id, - }), - ).rejects.toThrowError(BadRequestException); - }); - }); - describe('getStatistics', () => { it('should get the statistics for a user, excluding archived assets', async () => { assetMock.getStatistics.mockResolvedValue(stats); @@ -548,19 +421,19 @@ describe(AssetService.name, () => { await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf( BadRequestException, ); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should update the asset', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - assetMock.save.mockResolvedValue(assetStub.image); + assetMock.getById.mockResolvedValue(assetStub.image); await sut.update(authStub.admin, 'asset-1', { isFavorite: true }); - expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); }); it('should update the exif description', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - assetMock.save.mockResolvedValue(assetStub.image); + assetMock.getById.mockResolvedValue(assetStub.image); await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' }); }); @@ -715,7 +588,7 @@ describe(AssetService.name, () => { stackParentId: 'parent', }); - expect(communicationMock.send).toHaveBeenCalledWith(ClientEvent.ASSET_STACK_UPDATE, authStub.user1.user.id, [ + expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_STACK_UPDATE, authStub.user1.user.id, [ 'asset-1', 'parent', ]); @@ -788,8 +661,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.webpPath, - assetWithFace.resizePath, + assetWithFace.thumbnailPath, + assetWithFace.previewPath, assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, @@ -872,8 +745,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetStub.external.webpPath, - assetStub.external.resizePath, + assetStub.external.thumbnailPath, + assetStub.external.previewPath, assetStub.external.encodedVideoPath, assetStub.external.sidecarPath, ], @@ -955,9 +828,7 @@ describe(AssetService.name, () => { it('should run the refresh thumbnails job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - ]); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); }); it('should run the transcode video', async () => { diff --git a/server/src/domain/asset/asset.service.ts b/server/src/services/asset.service.ts similarity index 75% rename from server/src/domain/asset/asset.service.ts rename to server/src/services/asset.service.ts index e54eb8439..135377e0b 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,61 +1,52 @@ -import { AssetEntity, LibraryType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { AccessCore, Permission } from '../access'; -import { AuthDto } from '../auth'; -import { mimeTypes } from '../domain.constant'; -import { usePagination } from '../domain.util'; -import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; import { - ClientEvent, - IAccessRepository, - IAssetRepository, - IAssetStackRepository, - ICommunicationRepository, - IJobRepository, - IPartnerRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, - JobItem, - JobStatus, - TimeBucketOptions, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; -import { SystemConfigCore } from '../system-config'; + AssetResponseDto, + MemoryLaneResponseDto, + SanitizedAssetResponseDto, + mapAsset, +} from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, AssetBulkUpdateDto, AssetJobName, AssetJobsDto, AssetStatsDto, - MapMarkerDto, - MemoryLaneDto, - TimeBucketAssetDto, - TimeBucketDto, UpdateAssetDto, - UpdateStackParentDto, + UploadFieldName, mapStats, -} from './dto'; +} from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto'; +import { UpdateStackParentDto } from 'src/dtos/stack.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { LibraryType } from 'src/entities/library.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { - AssetResponseDto, - MapMarkerResponseDto, - MemoryLaneResponseDto, - SanitizedAssetResponseDto, - TimeBucketResponseDto, - mapAsset, -} from './response-dto'; - -export enum UploadFieldName { - ASSET_DATA = 'assetData', - LIVE_PHOTO_DATA = 'livePhotoData', - SIDECAR_DATA = 'sidecarData', - PROFILE_DATA = 'file', -} + IAssetDeletionJob, + IJobRepository, + ISidecarWriteJob, + JOBS_ASSET_PAGINATION_SIZE, + JobItem, + JobName, + JobStatus, +} from 'src/interfaces/job.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; export interface UploadRequest { auth: AuthDto | null; @@ -83,7 +74,7 @@ export class AssetService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository, ) { @@ -182,86 +173,25 @@ export class AssetService { userIds.push(...partnersIds); const assets = await this.assetRepository.getByDayOfYear(userIds, dto); - - return _.chain(assets) - .filter((asset) => asset.localDateTime.getFullYear() < currentYear) - .map((asset) => { - const years = currentYear - asset.localDateTime.getFullYear(); - - return { - title: `${years} year${years > 1 ? 's' : ''} since...`, - asset: mapAsset(asset, { auth }), - }; - }) - .groupBy((asset) => asset.title) - .map((items, title) => ({ title, assets: items.map(({ asset }) => asset) })) - .value(); - } - - private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { - if (dto.albumId) { - await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); - } else { - dto.userId = dto.userId || auth.user.id; - } - - if (dto.userId) { - await this.access.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); - if (dto.isArchived !== false) { - await this.access.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); + const groups: Record = {}; + for (const asset of assets) { + const yearsAgo = currentYear - asset.localDateTime.getFullYear(); + if (!groups[yearsAgo]) { + groups[yearsAgo] = []; } + groups[yearsAgo].push(asset); } - if (dto.withPartners) { - const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; - const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; - const requestedTrash = dto.isTrashed === true; - - if (requestedArchived || requestedFavorite || requestedTrash) { - throw new BadRequestException( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', - ); - } - } - } - - async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { - await this.timeBucketChecks(auth, dto); - const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - - return this.assetRepository.getTimeBuckets(timeBucketOptions); - } - - async getTimeBucket( - auth: AuthDto, - dto: TimeBucketAssetDto, - ): Promise { - await this.timeBucketChecks(auth, dto); - const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); - return !auth.sharedLink || auth.sharedLink?.showExif - ? assets.map((asset) => mapAsset(asset, { withStack: true, auth })) - : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth })); - } - - async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { - const { userId, ...options } = dto; - let userIds: string[] | undefined = undefined; - - if (userId) { - userIds = [userId]; - - if (dto.withPartners) { - const partners = await this.partnerRepository.getAll(auth.user.id); - const partnersIds = partners - .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) - .map((partner) => partner.sharedById); - - userIds.push(...partnersIds); - } - } - - return { ...options, userIds }; + return Object.keys(groups) + .map(Number) + .sort((a, b) => a - b) + .filter((yearsAgo) => yearsAgo > 0) + .map((yearsAgo) => ({ + yearsAgo, + // TODO move this to clients + title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`, + assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })), + })); } async getStatistics(auth: AuthDto, dto: AssetStatsDto) { @@ -324,7 +254,19 @@ export class AssetService { const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); - const asset = await this.assetRepository.save({ id, ...rest }); + await this.assetRepository.update({ id, ...rest }); + const asset = await this.assetRepository.getById(id, { + exifInfo: true, + owner: true, + smartInfo: true, + tags: true, + faces: { + person: true, + }, + }); + if (!asset) { + throw new BadRequestException('Asset not found'); + } return mapAsset(asset, { auth }); } @@ -391,7 +333,7 @@ export class AssetService { .flatMap((stack) => (stack ? [stack] : [])) .filter((stack) => stack.assets.length < 2); await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id))); - this.communicationRepository.send(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids); + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids); } async handleAssetDeletionCheck(): Promise { @@ -450,14 +392,14 @@ export class AssetService { await this.assetRepository.remove(asset); await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0)); - this.communicationRepository.send(ClientEvent.ASSET_DELETE, asset.ownerId, id); + this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id); // TODO refactor this to use cascades if (asset.livePhotoVideoId) { await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); } - const files = [asset.webpPath, asset.resizePath, asset.encodedVideoPath, asset.sidecarPath]; + const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath, asset.sidecarPath]; if (!fromExternal) { files.push(asset.originalPath); } @@ -478,7 +420,7 @@ export class AssetService { await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } }))); } else { await this.assetRepository.softDeleteAll(ids); - this.communicationRepository.send(ClientEvent.ASSET_TRASH, auth.user.id, ids); + this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, ids); } } @@ -509,7 +451,7 @@ export class AssetService { primaryAssetId: newParentId, }); - this.communicationRepository.send(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, [ + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, [ ...childIds, newParentId, oldParentId, @@ -530,7 +472,7 @@ export class AssetService { } case AssetJobName.REGENERATE_THUMBNAIL: { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } }); + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } }); break; } diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/services/audit.service.spec.ts similarity index 63% rename from server/src/domain/audit/audit.service.spec.ts rename to server/src/services/audit.service.spec.ts index 82c6cc699..4af5c1f94 100644 --- a/server/src/domain/audit/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,26 +1,21 @@ -import { DatabaseAction, EntityType } from '@app/infra/entities'; -import { - IAccessRepositoryMock, - auditStub, - authStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newAuditRepositoryMock, - newCryptoRepositoryMock, - newPersonRepositoryMock, - newStorageRepositoryMock, - newUserRepositoryMock, -} from '@test'; -import { - IAssetRepository, - IAuditRepository, - ICryptoRepository, - IPersonRepository, - IStorageRepository, - IUserRepository, - JobStatus, -} from '../repositories'; -import { AuditService } from './audit.service'; +import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AuditService } from 'src/services/audit.service'; +import { auditStub } from 'test/fixtures/audit.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; describe(AuditService.name, () => { let sut: AuditService; diff --git a/server/src/domain/audit/audit.service.ts b/server/src/services/audit.service.ts similarity index 75% rename from server/src/domain/audit/audit.service.ts rename to server/src/services/audit.service.ts index 91ebd78ee..d40167429 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -1,24 +1,9 @@ -import { AssetPathType, DatabaseAction, PersonPathType, UserPathType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; -import { AccessCore, Permission } from '../access'; -import { AuthDto } from '../auth'; -import { AUDIT_LOG_MAX_DURATION } from '../domain.constant'; -import { usePagination } from '../domain.util'; -import { JOBS_ASSET_PAGINATION_SIZE } from '../job'; -import { - IAccessRepository, - IAssetRepository, - IAuditRepository, - ICryptoRepository, - IPersonRepository, - IStorageRepository, - IUserRepository, - JobStatus, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; +import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AuditDeletesDto, AuditDeletesResponseDto, @@ -26,7 +11,20 @@ import { FileChecksumResponseDto, FileReportItemDto, PathEntityType, -} from './audit.dto'; +} from 'src/dtos/audit.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { DatabaseAction } from 'src/entities/audit.entity'; +import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService { @@ -93,27 +91,27 @@ export class AuditService { switch (pathType) { case AssetPathType.ENCODED_VIDEO: { - await this.assetRepository.save({ id, encodedVideoPath: pathValue }); + await this.assetRepository.update({ id, encodedVideoPath: pathValue }); break; } - case AssetPathType.JPEG_THUMBNAIL: { - await this.assetRepository.save({ id, resizePath: pathValue }); + case AssetPathType.PREVIEW: { + await this.assetRepository.update({ id, previewPath: pathValue }); break; } - case AssetPathType.WEBP_THUMBNAIL: { - await this.assetRepository.save({ id, webpPath: pathValue }); + case AssetPathType.THUMBNAIL: { + await this.assetRepository.update({ id, thumbnailPath: pathValue }); break; } case AssetPathType.ORIGINAL: { - await this.assetRepository.save({ id, originalPath: pathValue }); + await this.assetRepository.update({ id, originalPath: pathValue }); break; } case AssetPathType.SIDECAR: { - await this.assetRepository.save({ id, sidecarPath: pathValue }); + await this.assetRepository.update({ id, sidecarPath: pathValue }); break; } @@ -176,8 +174,8 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) { - for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) { + for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { + for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { track(file); } @@ -193,14 +191,14 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (resizePath && !hasFile(thumbFiles, resizePath)) { - orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath }); + if (previewPath && !hasFile(thumbFiles, previewPath)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); } - if (webpPath && !hasFile(thumbFiles, webpPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath }); + if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: encodedVideoPath }); + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); } } } diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/services/auth.service.spec.ts similarity index 92% rename from server/src/domain/auth/auth.service.spec.ts rename to server/src/services/auth.service.spec.ts index 214b6748e..30773f3f1 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -1,38 +1,32 @@ -import { UserEntity } from '@app/infra/entities'; import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - authStub, - keyStub, - loginResponseStub, - newAccessRepositoryMock, - newCryptoRepositoryMock, - newKeyRepositoryMock, - newLibraryRepositoryMock, - newSharedLinkRepositoryMock, - newSystemConfigRepositoryMock, - newUserRepositoryMock, - newUserTokenRepositoryMock, - sharedLinkStub, - systemConfigStub, - userStub, - userTokenStub, -} from '@test'; import { IncomingHttpHeaders } from 'node:http'; import { Issuer, generators } from 'openid-client'; import { Socket } from 'socket.io'; -import { - ICryptoRepository, - IKeyRepository, - ILibraryRepository, - ISharedLinkRepository, - ISystemConfigRepository, - IUserRepository, - IUserTokenRepository, -} from '../repositories'; -import { AuthType } from './auth.constant'; -import { AuthDto, SignUpDto } from './auth.dto'; -import { AuthService } from './auth.service'; +import { AuthType } from 'src/constants'; +import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; +import { UserEntity } from 'src/entities/user.entity'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AuthService } from 'src/services/auth.service'; +import { keyStub } from 'test/fixtures/api-key.stub'; +import { authStub, loginResponseStub } from 'test/fixtures/auth.stub'; +import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; +import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userTokenStub } from 'test/fixtures/user-token.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; +import { newUserTokenRepositoryMock } from 'test/repositories/user-token.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); diff --git a/server/src/domain/auth/auth.service.ts b/server/src/services/auth.service.ts similarity index 93% rename from server/src/domain/auth/auth.service.ts rename to server/src/services/auth.service.ts index fe01ef39b..19476667c 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,5 +1,3 @@ -import { SystemConfig, UserEntity } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, @@ -12,20 +10,6 @@ import cookieParser from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; -import { AccessCore, Permission } from '../access'; -import { HumanReadableSize } from '../domain.util'; -import { - IAccessRepository, - ICryptoRepository, - IKeyRepository, - ILibraryRepository, - ISharedLinkRepository, - ISystemConfigRepository, - IUserRepository, - IUserTokenRepository, -} from '../repositories'; -import { SystemConfigCore } from '../system-config/system-config.core'; -import { UserCore, UserResponseDto, mapUser } from '../user'; import { AuthType, IMMICH_ACCESS_COOKIE, @@ -34,7 +18,10 @@ import { IMMICH_IS_AUTHENTICATED, LOGIN_URL, MOBILE_REDIRECT, -} from './auth.constant'; +} from 'src/constants'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { UserCore } from 'src/cores/user.core'; import { AuthDeviceResponseDto, AuthDto, @@ -48,7 +35,20 @@ import { SignUpDto, mapLoginResponse, mapUserToken, -} from './auth.dto'; +} from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { SystemConfig } from 'src/entities/system-config.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { HumanReadableSize } from 'src/utils/bytes'; +import { ImmichLogger } from 'src/utils/logger'; export interface LoginDetails { isSecure: boolean; @@ -146,7 +146,6 @@ export class AuthService { async adminSignUp(dto: SignUpDto): Promise { const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { throw new BadRequestException('The server already has an admin'); } @@ -427,7 +426,7 @@ export class AuthService { } private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) { - const key = this.cryptoRepository.randomBytes(32).toString('base64').replaceAll(/\W/g, ''); + const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); await this.userTokenRepository.create({ diff --git a/server/src/domain/database/database.service.spec.ts b/server/src/services/database.service.spec.ts similarity index 96% rename from server/src/domain/database/database.service.spec.ts rename to server/src/services/database.service.spec.ts index 14464c0cd..6fa5e7fd8 100644 --- a/server/src/domain/database/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,13 +1,8 @@ -import { - DatabaseExtension, - DatabaseService, - IDatabaseRepository, - VectorIndex, - Version, - VersionType, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; -import { newDatabaseRepositoryMock } from '@test'; +import { DatabaseExtension, IDatabaseRepository, VectorIndex } from 'src/interfaces/database.interface'; +import { DatabaseService } from 'src/services/database.service'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; describe(DatabaseService.name, () => { let sut: DatabaseService; diff --git a/server/src/domain/database/database.service.ts b/server/src/services/database.service.ts similarity index 97% rename from server/src/domain/database/database.service.ts rename to server/src/services/database.service.ts index 946c6dac8..a333c0053 100644 --- a/server/src/domain/database/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,6 +1,4 @@ -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { Version, VersionType } from '../domain.constant'; import { DatabaseExtension, DatabaseLock, @@ -8,7 +6,9 @@ import { VectorExtension, VectorIndex, extName, -} from '../repositories'; +} from 'src/interfaces/database.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; @Injectable() export class DatabaseService { diff --git a/server/src/domain/download/download.service.spec.ts b/server/src/services/download.service.spec.ts similarity index 92% rename from server/src/domain/download/download.service.spec.ts rename to server/src/services/download.service.spec.ts index 09161d8f6..babc21fa8 100644 --- a/server/src/domain/download/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -1,18 +1,16 @@ import { BadRequestException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - assetStub, - authStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newStorageRepositoryMock, -} from '@test'; import { when } from 'jest-when'; +import { DownloadResponseDto } from 'src/dtos/download.dto'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { DownloadService } from 'src/services/download.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools.js'; -import { CacheControl, ImmichFileResponse } from '../domain.util'; -import { IAssetRepository, IStorageRepository } from '../repositories'; -import { DownloadResponseDto } from './download.dto'; -import { DownloadService } from './download.service'; const downloadResponse: DownloadResponseDto = { totalSize: 105_000, diff --git a/server/src/domain/download/download.service.ts b/server/src/services/download.service.ts similarity index 86% rename from server/src/domain/download/download.service.ts rename to server/src/services/download.service.ts index 1b4a19185..b0b68a1e8 100644 --- a/server/src/domain/download/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,13 +1,17 @@ -import { AssetEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; -import { AccessCore, Permission } from '../access'; -import { AssetIdsDto } from '../asset'; -import { AuthDto } from '../auth'; -import { mimeTypes } from '../domain.constant'; -import { CacheControl, HumanReadableSize, ImmichFileResponse, usePagination } from '../domain.util'; -import { IAccessRepository, IAssetRepository, IStorageRepository, ImmichReadStream } from '../repositories'; -import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from './download.dto'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface'; +import { HumanReadableSize } from 'src/utils/bytes'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class DownloadService { diff --git a/server/src/services/index.ts b/server/src/services/index.ts new file mode 100644 index 000000000..3c903c927 --- /dev/null +++ b/server/src/services/index.ts @@ -0,0 +1,61 @@ +import { ActivityService } from 'src/services/activity.service'; +import { AlbumService } from 'src/services/album.service'; +import { APIKeyService } from 'src/services/api-key.service'; +import { ApiService } from 'src/services/api.service'; +import { AssetServiceV1 } from 'src/services/asset-v1.service'; +import { AssetService } from 'src/services/asset.service'; +import { AuditService } from 'src/services/audit.service'; +import { AuthService } from 'src/services/auth.service'; +import { DatabaseService } from 'src/services/database.service'; +import { DownloadService } from 'src/services/download.service'; +import { JobService } from 'src/services/job.service'; +import { LibraryService } from 'src/services/library.service'; +import { MediaService } from 'src/services/media.service'; +import { MemoryService } from 'src/services/memory.service'; +import { MetadataService } from 'src/services/metadata.service'; +import { MicroservicesService } from 'src/services/microservices.service'; +import { PartnerService } from 'src/services/partner.service'; +import { PersonService } from 'src/services/person.service'; +import { SearchService } from 'src/services/search.service'; +import { ServerInfoService } from 'src/services/server-info.service'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import { SmartInfoService } from 'src/services/smart-info.service'; +import { StorageTemplateService } from 'src/services/storage-template.service'; +import { StorageService } from 'src/services/storage.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { TagService } from 'src/services/tag.service'; +import { TimelineService } from 'src/services/timeline.service'; +import { TrashService } from 'src/services/trash.service'; +import { UserService } from 'src/services/user.service'; + +export const services = [ + ApiService, + MicroservicesService, + APIKeyService, + ActivityService, + AlbumService, + AssetService, + AssetServiceV1, + AuditService, + AuthService, + DatabaseService, + DownloadService, + JobService, + LibraryService, + MediaService, + MemoryService, + MetadataService, + PartnerService, + PersonService, + SearchService, + ServerInfoService, + SharedLinkService, + SmartInfoService, + StorageService, + StorageTemplateService, + SystemConfigService, + TagService, + TimelineService, + TrashService, + UserService, +]; diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/services/job.service.spec.ts similarity index 86% rename from server/src/domain/job/job.service.spec.ts rename to server/src/services/job.service.spec.ts index c2133a623..ac0e502ae 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,26 +1,28 @@ -import { SystemConfig, SystemConfigKey } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; +import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; +import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { - assetStub, - newAssetRepositoryMock, - newCommunicationRepositoryMock, - newJobRepositoryMock, - newPersonRepositoryMock, - newSystemConfigRepositoryMock, -} from '@test'; -import { - IAssetRepository, - ICommunicationRepository, IJobRepository, - IPersonRepository, - ISystemConfigRepository, + JobCommand, JobHandler, JobItem, + JobName, JobStatus, -} from '../repositories'; -import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core'; -import { JobCommand, JobName, QueueName } from './job.constants'; -import { JobService } from './job.service'; + QueueName, +} from 'src/interfaces/job.interface'; +import { IMetricRepository } from 'src/interfaces/metric.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { JobService } from 'src/services/job.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; const makeMockHandlers = (status: JobStatus) => { const mock = jest.fn().mockResolvedValue(status); @@ -34,17 +36,19 @@ describe(JobService.name, () => { let sut: JobService; let assetMock: jest.Mocked; let configMock: jest.Mocked; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; let jobMock: jest.Mocked; let personMock: jest.Mocked; + let metricMock: jest.Mocked; beforeEach(() => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); + eventMock = newEventRepositoryMock(); jobMock = newJobRepositoryMock(); personMock = newPersonRepositoryMock(); - sut = new JobService(assetMock, communicationMock, jobMock, configMock, personMock); + metricMock = newMetricRepositoryMock(); + sut = new JobService(assetMock, eventMock, jobMock, configMock, personMock, metricMock); }); it('should work', () => { @@ -275,7 +279,7 @@ describe(JobService.name, () => { }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.GENERATE_JPEG_THUMBNAIL], + jobs: [JobName.GENERATE_PREVIEW], }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, @@ -286,24 +290,24 @@ describe(JobService.name, () => { jobs: [], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - jobs: [JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL], + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }, + jobs: [JobName.GENERATE_THUMBNAIL, JobName.GENERATE_THUMBHASH], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, ], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-live-image', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-live-image', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, @@ -325,7 +329,7 @@ describe(JobService.name, () => { for (const { item, jobs } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { - if (item.name === JobName.GENERATE_JPEG_THUMBNAIL && item.data.source === 'upload') { + if (item.name === JobName.GENERATE_PREVIEW && item.data.source === 'upload') { if (item.data.id === 'asset-live-image') { assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); } else { diff --git a/server/src/domain/job/job.service.ts b/server/src/services/job.service.ts similarity index 81% rename from server/src/domain/job/job.service.ts rename to server/src/services/job.service.ts index e00636ad6..3f9cd8a22 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,22 +1,26 @@ -import { AssetType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { mapAsset } from '../asset'; +import { snakeCase } from 'lodash'; +import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { - ClientEvent, - IAssetRepository, - ICommunicationRepository, + ConcurrentQueueName, IJobRepository, - IPersonRepository, - ISystemConfigRepository, + JobCommand, JobHandler, JobItem, + JobName, JobStatus, QueueCleanType, -} from '../repositories'; -import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core'; -import { ConcurrentQueueName, JobCommand, JobName, QueueName } from './job.constants'; -import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto'; + QueueName, +} from 'src/interfaces/job.interface'; +import { IMetricRepository } from 'src/interfaces/metric.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class JobService { @@ -25,10 +29,11 @@ export class JobService { constructor( @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, + @Inject(IMetricRepository) private metricRepository: IMetricRepository, ) { this.configCore = SystemConfigCore.create(configRepository); } @@ -90,6 +95,8 @@ export class JobService { throw new BadRequestException(`Job is already running`); } + this.metricRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1); + switch (name) { case QueueName.VIDEO_CONVERSION: { return this.jobRepository.queue({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force } }); @@ -154,14 +161,21 @@ export class JobService { this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise => { const { name, data } = item; + const queueMetric = `immich.queues.${snakeCase(queueName)}.active`; + this.metricRepository.jobs.addToGauge(queueMetric, 1); + try { const handler = jobHandlers[name]; const status = await handler(data); + const jobMetric = `immich.jobs.${name.replaceAll('-', '_')}.${status}`; + this.metricRepository.jobs.addToCounter(jobMetric, 1); if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) { await this.onDone(item); } } catch (error: Error | any) { this.logger.error(`Unable to run job handler (${queueName}/${name}): ${error}`, error?.stack, data); + } finally { + this.metricRepository.jobs.addToGauge(queueMetric, -1); } }); } @@ -217,7 +231,7 @@ export class JobService { if (item.data.source === 'sidecar-write') { const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); if (asset) { - this.communicationRepository.send(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset)); } } await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); @@ -231,7 +245,7 @@ export class JobService { case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { if (item.data.source === 'upload') { - await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data }); + await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data }); } break; } @@ -240,15 +254,15 @@ export class JobService { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { - this.communicationRepository.send(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); + this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); } break; } - case JobName.GENERATE_JPEG_THUMBNAIL: { + case JobName.GENERATE_PREVIEW: { const jobs: JobItem[] = [ - { name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, - { name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBHASH, data: item.data }, ]; if (item.data.source === 'upload') { @@ -268,7 +282,7 @@ export class JobService { break; } - case JobName.GENERATE_WEBP_THUMBNAIL: { + case JobName.GENERATE_THUMBNAIL: { if (item.data.source !== 'upload') { break; } @@ -277,13 +291,13 @@ export class JobService { // Only live-photo motion part will be marked as not visible immediately on upload. Skip notifying clients if (asset && asset.isVisible) { - this.communicationRepository.send(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); } break; } case JobName.USER_DELETION: { - this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id); + this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id); break; } } diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/services/library.service.spec.ts similarity index 93% rename from server/src/domain/library/library.service.spec.ts rename to server/src/services/library.service.spec.ts index 3b5258b97..df59d0c15 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1,45 +1,37 @@ -import { AssetType, LibraryType, SystemConfig, SystemConfigKey, UserEntity } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - assetStub, - authStub, - libraryStub, - makeMockWatcher, - newAccessRepositoryMock, - newAssetRepositoryMock, - newCryptoRepositoryMock, - newDatabaseRepositoryMock, - newJobRepositoryMock, - newLibraryRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - systemConfigStub, - userStub, -} from '@test'; import { when } from 'jest-when'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; -import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; -import { - IAssetRepository, - ICryptoRepository, - IDatabaseRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - ISystemConfigRepository, - JobStatus, - StorageEventType, -} from '../repositories'; -import { SystemConfigCore } from '../system-config/system-config.core'; -import { mapLibrary } from './library.dto'; -import { LibraryService } from './library.service'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { mapLibrary } from 'src/dtos/library.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { LibraryType } from 'src/entities/library.entity'; +import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { LibraryService } from 'src/services/library.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { libraryStub } from 'test/fixtures/library.stub'; +import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; describe(LibraryService.name, () => { let sut: LibraryService; - let accessMock: IAccessRepositoryMock; let assetMock: jest.Mocked; let configMock: jest.Mocked; let cryptoMock: jest.Mocked; @@ -49,7 +41,6 @@ describe(LibraryService.name, () => { let databaseMock: jest.Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); configMock = newSystemConfigRepositoryMock(); libraryMock = newLibraryRepositoryMock(); assetMock = newAssetRepositoryMock(); @@ -58,19 +49,7 @@ describe(LibraryService.name, () => { storageMock = newStorageRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); - // Always validate owner access for library. - accessMock.library.checkOwnerAccess.mockImplementation((_, libraryIds) => Promise.resolve(libraryIds)); - - sut = new LibraryService( - accessMock, - assetMock, - configMock, - cryptoMock, - jobMock, - libraryMock, - storageMock, - databaseMock, - ); + sut = new LibraryService(assetMock, configMock, cryptoMock, jobMock, libraryMock, storageMock, databaseMock); databaseMock.tryLock.mockResolvedValue(true); }); @@ -148,10 +127,10 @@ describe(LibraryService.name, () => { }); }); - describe('validateConfig', () => { + describe('onValidateConfig', () => { it('should allow a valid cron expression', () => { expect(() => - sut.validateConfig({ + sut.onValidateConfig({ newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -160,7 +139,7 @@ describe(LibraryService.name, () => { it('should fail for an invalid cron expression', () => { expect(() => - sut.validateConfig({ + sut.onValidateConfig({ newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -377,7 +356,7 @@ describe(LibraryService.name, () => { fileModifiedAt: expect.any(Date), localDateTime: expect.any(Date), type: AssetType.IMAGE, - originalFileName: 'photo', + originalFileName: 'photo.jpg', sidecarPath: null, isReadOnly: true, isExternal: true, @@ -425,7 +404,7 @@ describe(LibraryService.name, () => { fileModifiedAt: expect.any(Date), localDateTime: expect.any(Date), type: AssetType.IMAGE, - originalFileName: 'photo', + originalFileName: 'photo.jpg', sidecarPath: '/data/user1/photo.jpg.xmp', isReadOnly: true, isExternal: true, @@ -472,7 +451,7 @@ describe(LibraryService.name, () => { fileModifiedAt: expect.any(Date), localDateTime: expect.any(Date), type: AssetType.VIDEO, - originalFileName: 'video', + originalFileName: 'video.mp4', sidecarPath: null, isReadOnly: true, isExternal: true, @@ -522,17 +501,17 @@ describe(LibraryService.name, () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, - assetPath: '/data/user1/photo.jpg', + assetPath: assetStub.hasFileExtension.originalPath, force: false, }; storageMock.stat.mockResolvedValue({ size: 100, - mtime: assetStub.image.fileModifiedAt, + mtime: assetStub.hasFileExtension.fileModifiedAt, ctime: new Date('2023-01-01'), } as Stats); - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); @@ -569,6 +548,26 @@ describe(LibraryService.name, () => { }); }); + it('should import an asset that is missing a file extension', async () => { + // This tests for the case where the file extension is missing from the asset path. + // This happened in previous versions of Immich + const mockLibraryJob: ILibraryFileJob = { + id: libraryStub.externalLibrary1.id, + ownerId: mockUser.id, + assetPath: assetStub.missingFileExtension.originalPath, + force: false, + }; + + assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension); + + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.updateAll).toHaveBeenCalledWith( + [assetStub.missingFileExtension.id], + expect.objectContaining({ originalFileName: 'photo.jpg' }), + ); + }); + it('should set a missing asset to offline', async () => { storageMock.stat.mockRejectedValue(new Error('Path not found')); @@ -584,7 +583,7 @@ describe(LibraryService.name, () => { await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true }); + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true }); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); }); @@ -602,7 +601,7 @@ describe(LibraryService.name, () => { await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false }); + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, @@ -631,7 +630,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); }); @@ -639,19 +638,20 @@ describe(LibraryService.name, () => { it('should refresh an existing asset if forced', async () => { const mockLibraryJob: ILibraryFileJob = { id: assetStub.image.id, - ownerId: assetStub.image.ownerId, - assetPath: '/data/user1/photo.jpg', + ownerId: assetStub.hasFileExtension.ownerId, + assetPath: assetStub.hasFileExtension.originalPath, force: true, }; - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); - assetMock.create.mockResolvedValue(assetStub.image); + assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension); + assetMock.create.mockResolvedValue(assetStub.hasFileExtension); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], { + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], { fileCreatedAt: new Date('2023-01-01'), fileModifiedAt: new Date('2023-01-01'), + originalFileName: assetStub.hasFileExtension.originalFileName, }); }); @@ -1257,7 +1257,7 @@ describe(LibraryService.name, () => { await sut.watchAll(); - expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); }); it('should handle an error event', async () => { diff --git a/server/src/domain/library/library.service.ts b/server/src/services/library.service.ts similarity index 91% rename from server/src/domain/library/library.service.ts rename to server/src/services/library.service.ts index 000acac29..d0e41da17 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,35 +1,13 @@ -import { AssetType, LibraryEntity, LibraryType } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { Trie } from 'mnemonist'; import { R_OK } from 'node:constants'; import { EventEmitter } from 'node:events'; import { Stats } from 'node:fs'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; -import { AccessCore } from '../access'; -import { mimeTypes } from '../domain.constant'; -import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util'; -import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; -import { - DatabaseLock, - IAccessRepository, - IAssetRepository, - ICryptoRepository, - IDatabaseRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - ISystemConfigRepository, - InternalEvent, - InternalEventMap, - JobStatus, - StorageEventType, - WithProperty, -} from '../repositories'; -import { StorageCore } from '../storage'; -import { SystemConfigCore } from '../system-config'; +import { StorageCore } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnServerEvent } from 'src/decorators'; import { CreateLibraryDto, LibraryResponseDto, @@ -41,21 +19,43 @@ import { ValidateLibraryImportPathResponseDto, ValidateLibraryResponseDto, mapLibrary, -} from './library.dto'; +} from 'src/dtos/library.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; +import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { ServerAsyncEvent, ServerAsyncEventMap } from 'src/interfaces/event.interface'; +import { + IBaseJob, + IEntityJob, + IJobRepository, + ILibraryFileJob, + ILibraryRefreshJob, + JOBS_ASSET_PAGINATION_SIZE, + JobName, + JobStatus, +} from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { handlePromiseError } from 'src/utils/misc'; +import { usePagination } from 'src/utils/pagination'; +import { validateCronExpression } from 'src/validation'; const LIBRARY_SCAN_BATCH_SIZE = 5000; @Injectable() export class LibraryService extends EventEmitter { readonly logger = new ImmichLogger(LibraryService.name); - private access: AccessCore; private configCore: SystemConfigCore; private watchLibraries = false; private watchLock = false; private watchers: Record Promise> = {}; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -65,7 +65,6 @@ export class LibraryService extends EventEmitter { @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, ) { super(); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(configRepository); } @@ -106,8 +105,8 @@ export class LibraryService extends EventEmitter { }); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) - validateConfig({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { + @OnServerEvent(ServerAsyncEvent.CONFIG_VALIDATE) + onValidateConfig({ newConfig }: ServerAsyncEventMap[ServerAsyncEvent.CONFIG_VALIDATE]) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { throw new Error(`Invalid cron expression ${scan.cronExpression}`); @@ -173,7 +172,7 @@ export class LibraryService extends EventEmitter { this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); if (asset && matcher(path)) { - await this.assetRepository.save({ id: asset.id, isOffline: true }); + await this.assetRepository.update({ id: asset.id, isOffline: true }); } this.emit(StorageEventType.UNLINK, path); }; @@ -429,7 +428,7 @@ export class LibraryService extends EventEmitter { // Mark asset as offline this.logger.debug(`Marking asset as offline: ${assetPath}`); - await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true }); + await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: true }); return JobStatus.SUCCESS; } else { // File can't be accessed and does not already exist in db @@ -444,6 +443,8 @@ export class LibraryService extends EventEmitter { doRefresh = true; } + const originalFileName = parse(assetPath).base; + if (!existingAssetEntity) { // This asset is new to us, read it from disk this.logger.debug(`Importing new asset: ${assetPath}`); @@ -454,6 +455,12 @@ export class LibraryService extends EventEmitter { `File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`, ); doRefresh = true; + } else if (existingAssetEntity.originalFileName !== originalFileName) { + // TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users + this.logger.debug( + `Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`, + ); + doRefresh = true; } else if (!job.force && stats && !existingAssetEntity.isOffline) { // Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`); @@ -462,7 +469,7 @@ export class LibraryService extends EventEmitter { if (stats && existingAssetEntity?.isOffline) { // File was previously offline but is now online this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`); - await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: false }); + await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: false }); doRefresh = true; } @@ -511,7 +518,7 @@ export class LibraryService extends EventEmitter { fileModifiedAt: stats.mtime, localDateTime: stats.mtime, type: assetType, - originalFileName: parse(assetPath).name, + originalFileName, sidecarPath, isReadOnly: true, isExternal: true, @@ -522,6 +529,7 @@ export class LibraryService extends EventEmitter { await this.assetRepository.updateAll([existingAssetEntity.id], { fileCreatedAt: stats.mtime, fileModifiedAt: stats.mtime, + originalFileName, }); } else { // Not importing and not refreshing, do nothing diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/services/media.service.spec.ts similarity index 93% rename from server/src/domain/media/media.service.spec.ts rename to server/src/services/media.service.spec.ts index beea126bf..3a650430e 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,43 +1,37 @@ +import { Stats } from 'node:fs'; +import { AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; import { - AssetType, AudioCodec, Colorspace, - ExifEntity, + ImageFormat, SystemConfigKey, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec, -} from '@app/infra/entities'; -import { - assetStub, - faceStub, - newAssetRepositoryMock, - newCryptoRepositoryMock, - newJobRepositoryMock, - newMediaRepositoryMock, - newMoveRepositoryMock, - newPersonRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - personStub, - probeStub, -} from '@test'; -import { Stats } from 'node:fs'; -import { JobName } from '../job'; -import { - IAssetRepository, - ICryptoRepository, - IJobRepository, - IMediaRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, - JobStatus, - WithoutProperty, -} from '../repositories'; -import { MediaService } from './media.service'; +} from 'src/entities/system-config.entity'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { MediaService } from 'src/services/media.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { faceStub } from 'test/fixtures/face.stub'; +import { probeStub } from 'test/fixtures/media.stub'; +import { personStub } from 'test/fixtures/person.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; +import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; describe(MediaService.name, () => { let sut: MediaService; @@ -85,7 +79,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -143,7 +137,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -167,7 +161,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_WEBP_THUMBNAIL, + name: JobName.GENERATE_THUMBNAIL, data: { id: assetStub.image.id }, }, ]); @@ -191,7 +185,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBHASH_THUMBNAIL, + name: JobName.GENERATE_THUMBHASH, data: { id: assetStub.image.id }, }, ]); @@ -200,36 +194,40 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateJpegThumbnail', () => { + describe('handleGeneratePreview', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalledWith(); + expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should skip video thumbnail generation if no video stream', async () => { mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalledWith(); + expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail for an image', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.SRGB, - }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -237,30 +235,34 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.P3, - }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should generate a thumbnail for a video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -271,21 +273,21 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should tonemap thumbnail for hdr video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -296,9 +298,9 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -309,11 +311,11 @@ describe(MediaService.name, () => { { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' }, ]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -328,31 +330,35 @@ describe(MediaService.name, () => { it('should run successfully', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); }); }); - describe('handleGenerateWebpThumbnail', () => { + describe('handleGenerateThumbnail', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalledWith(); + expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.SRGB, - }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); }); @@ -361,31 +367,35 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.P3, - }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); describe('handleGenerateThumbhashThumbnail', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); it('should skip thumbhash generation if resize path is missing', async () => { assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.noResizePath.id }); + await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); @@ -394,10 +404,10 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); - expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); }); }); diff --git a/server/src/domain/media/media.service.ts b/server/src/services/media.service.ts similarity index 82% rename from server/src/domain/media/media.service.ts rename to server/src/services/media.service.ts index 9d522d104..c56fd26e6 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,37 +1,36 @@ +import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetPathType } from 'src/entities/move.entity'; import { - AssetEntity, - AssetPathType, - AssetType, AudioCodec, Colorspace, + ImageFormat, TranscodeHWAccel, TranscodePolicy, TranscodeTarget, VideoCodec, -} from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; -import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; -import { usePagination } from '../domain.util'; -import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; +} from 'src/entities/system-config.entity'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { - AudioStreamInfo, - IAssetRepository, - ICryptoRepository, + IBaseJob, + IEntityJob, IJobRepository, - IMediaRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, + JOBS_ASSET_PAGINATION_SIZE, JobItem, + JobName, JobStatus, - VideoCodecHWConfig, - VideoStreamInfo, - WithoutProperty, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; -import { SystemConfigFFmpegDto } from '../system-config'; -import { SystemConfigCore } from '../system-config/system-config.core'; + QueueName, +} from 'src/interfaces/job.interface'; +import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; import { H264Config, HEVCConfig, @@ -41,7 +40,8 @@ import { ThumbnailConfig, VAAPIConfig, VP9Config, -} from './media.util'; +} from 'src/utils/media'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class MediaService { @@ -58,7 +58,7 @@ export class MediaService { @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, ) { this.configCore = SystemConfigCore.create(configRepository); this.storageCore = StorageCore.create( @@ -82,15 +82,15 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.resizePath || force) { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } }); + if (!asset.previewPath || force) { + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.webpPath) { - jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } }); + if (!asset.thumbnailPath) { + jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } if (!asset.thumbhash) { - jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } }); + jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } } @@ -153,41 +153,41 @@ export class MediaService { } async handleAssetMigration({ id }: IEntityJob): Promise { + const { image } = await this.configCore.getConfig(); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; } - await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); + await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat); + await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); + await this.storageCore.moveAssetVideo(asset); return JobStatus.SUCCESS; } - async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise { + async handleGeneratePreview({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const resizePath = await this.generateThumbnail(asset, 'jpeg'); - await this.assetRepository.save({ id: asset.id, resizePath }); + const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG); + await this.assetRepository.update({ id: asset.id, previewPath }); return JobStatus.SUCCESS; } - private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { - const { thumbnail, ffmpeg } = await this.configCore.getConfig(); - const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize; - const path = - format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset); + private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + const { image, ffmpeg } = await this.configCore.getConfig(); + const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize; + const path = StorageCore.getImagePath(asset, type, format); this.storageCore.ensureFolders(path); switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace; - const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality }; - await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + await this.mediaRepository.resize(asset.originalPath, path, imageOptions); break; } @@ -215,25 +215,25 @@ export class MediaService { return path; } - async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const webpPath = await this.generateThumbnail(asset, 'webp'); - await this.assetRepository.save({ id: asset.id, webpPath }); + const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP); + await this.assetRepository.update({ id: asset.id, thumbnailPath }); return JobStatus.SUCCESS; } - async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbhash({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath); - await this.assetRepository.save({ id: asset.id, thumbhash }); + const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); + await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; } @@ -286,7 +286,7 @@ export class MediaService { if (asset.encodedVideoPath) { this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } }); - await this.assetRepository.save({ id: asset.id, encodedVideoPath: null }); + await this.assetRepository.update({ id: asset.id, encodedVideoPath: null }); } return JobStatus.SKIPPED; @@ -321,7 +321,7 @@ export class MediaService { this.logger.log(`Successfully encoded ${asset.id}`); - await this.assetRepository.save({ id: asset.id, encodedVideoPath: output }); + await this.assetRepository.update({ id: asset.id, encodedVideoPath: output }); return JobStatus.SUCCESS; } diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts new file mode 100644 index 000000000..5f045ffde --- /dev/null +++ b/server/src/services/memory.service.spec.ts @@ -0,0 +1,214 @@ +import { BadRequestException } from '@nestjs/common'; +import { MemoryType } from 'src/entities/memory.entity'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { MemoryService } from 'src/services/memory.service'; +import { authStub } from 'test/fixtures/auth.stub'; +import { memoryStub } from 'test/fixtures/memory.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; + +describe(MemoryService.name, () => { + let accessMock: IAccessRepositoryMock; + let memoryMock: jest.Mocked; + let sut: MemoryService; + + beforeEach(() => { + accessMock = newAccessRepositoryMock(); + memoryMock = newMemoryRepositoryMock(); + + sut = new MemoryService(accessMock, memoryMock); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('search', () => { + it('should search memories', async () => { + memoryMock.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]); + await expect(sut.search(authStub.admin)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'memory1', assets: expect.any(Array) }), + expect.objectContaining({ id: 'memoryEmpty', assets: [] }), + ]), + ); + }); + + it('should map ', async () => { + await expect(sut.search(authStub.admin)).resolves.toEqual([]); + }); + }); + + describe('get', () => { + it('should throw an error when no access', async () => { + await expect(sut.get(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should throw an error when the memory is not found', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['race-condition'])); + await expect(sut.get(authStub.admin, 'race-condition')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should get a memory by id', async () => { + memoryMock.get.mockResolvedValue(memoryStub.memory1); + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.get(authStub.admin, 'memory1')).resolves.toMatchObject({ id: 'memory1' }); + expect(memoryMock.get).toHaveBeenCalledWith('memory1'); + expect(accessMock.memory.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['memory1'])); + }); + }); + + describe('create', () => { + it('should skip assets the user does not have access to', async () => { + memoryMock.create.mockResolvedValue(memoryStub.empty); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + assetIds: ['not-mine'], + memoryAt: new Date(2024), + }), + ).resolves.toMatchObject({ assets: [] }); + expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); + }); + + it('should create a memory', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.create.mockResolvedValue(memoryStub.memory1); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + assetIds: ['asset1'], + memoryAt: new Date(2024), + }), + ).resolves.toBeDefined(); + expect(memoryMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + ownerId: userStub.admin.id, + assets: [{ id: 'asset1' }], + }), + ); + }); + + it('should create a memory without assets', async () => { + memoryMock.create.mockResolvedValue(memoryStub.memory1); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + memoryAt: new Date(2024), + }), + ).resolves.toBeDefined(); + }); + }); + + describe('update', () => { + it('should require access', async () => { + await expect(sut.update(authStub.admin, 'not-found', { isSaved: true })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.update).not.toHaveBeenCalled(); + }); + + it('should update a memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.update.mockResolvedValue(memoryStub.memory1); + await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); + expect(memoryMock.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'memory1', + isSaved: true, + }), + ); + }); + }); + + describe('remove', () => { + it('should require access', async () => { + await expect(sut.remove(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException); + expect(memoryMock.delete).not.toHaveBeenCalled(); + }); + + it('should delete a memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.remove(authStub.admin, 'memory1')).resolves.toBeUndefined(); + expect(memoryMock.delete).toHaveBeenCalledWith('memory1'); + }); + }); + + describe('addAssets', () => { + it('should require memory access', async () => { + await expect(sut.addAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should require asset access', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([ + { error: 'no_permission', id: 'not-found', success: false }, + ]); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should skip assets already in the memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { error: 'duplicate', id: 'asset1', success: false }, + ]); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should add assets', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { id: 'asset1', success: true }, + ]); + expect(memoryMock.addAssetIds).toHaveBeenCalledWith('memory1', ['asset1']); + }); + }); + + describe('removeAssets', () => { + it('should require memory access', async () => { + await expect(sut.removeAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should skip assets not in the memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([ + { error: 'not_found', id: 'not-found', success: false }, + ]); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should require asset access', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { error: 'no_permission', id: 'asset1', success: false }, + ]); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should remove assets', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { id: 'asset1', success: true }, + ]); + expect(memoryMock.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']); + }); + }); +}); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts new file mode 100644 index 000000000..a73eb3ec0 --- /dev/null +++ b/server/src/services/memory.service.ts @@ -0,0 +1,105 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { addAssets, removeAssets } from 'src/utils/asset.util'; + +@Injectable() +export class MemoryService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IMemoryRepository) private repository: IMemoryRepository, + ) { + this.access = AccessCore.create(accessRepository); + } + + async search(auth: AuthDto) { + const memories = await this.repository.search(auth.user.id); + return memories.map((memory) => mapMemory(memory)); + } + + async get(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + const memory = await this.findOrFail(id); + return mapMemory(memory); + } + + async create(auth: AuthDto, dto: MemoryCreateDto) { + // TODO validate type/data combination + + const assetIds = dto.assetIds || []; + const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); + const memory = await this.repository.create({ + ownerId: auth.user.id, + type: dto.type, + data: dto.data, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), + }); + + return mapMemory(memory); + } + + async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + + const memory = await this.repository.update({ + id, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + }); + + return mapMemory(memory); + } + + async remove(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); + await this.repository.delete(id); + } + + async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + + const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const results = await addAssets(auth, repos, { id, assetIds: dto.ids }); + + const hasSuccess = results.find(({ success }) => success); + if (hasSuccess) { + await this.repository.update({ id, updatedAt: new Date() }); + } + + return results; + } + + async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + + const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const permissions = [Permission.ASSET_SHARE]; + const results = await removeAssets(auth, repos, { id, assetIds: dto.ids, permissions }); + + const hasSuccess = results.find(({ success }) => success); + if (hasSuccess) { + await this.repository.update({ id, updatedAt: new Date() }); + } + + return results; + } + + private async findOrFail(id: string) { + const memory = await this.repository.get(id); + if (!memory) { + throw new BadRequestException('Memory not found'); + } + return memory; + } +} diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts similarity index 87% rename from server/src/domain/metadata/metadata.service.spec.ts rename to server/src/services/metadata.service.spec.ts index c28c61f22..fd2c3e388 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1,46 +1,39 @@ -import { AssetType, ExifEntity, SystemConfigKey } from '@app/infra/entities'; -import { - assetStub, - fileStub, - newAlbumRepositoryMock, - newAssetRepositoryMock, - newCommunicationRepositoryMock, - newCryptoRepositoryMock, - newDatabaseRepositoryMock, - newJobRepositoryMock, - newMediaRepositoryMock, - newMetadataRepositoryMock, - newMoveRepositoryMock, - newPersonRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - probeStub, -} from '@test'; import { BinaryField } from 'exiftool-vendored'; import { when } from 'jest-when'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; -import { JobName } from '../job'; -import { - ClientEvent, - IAlbumRepository, - IAssetRepository, - ICommunicationRepository, - ICryptoRepository, - IDatabaseRepository, - IJobRepository, - IMediaRepository, - IMetadataRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, - ImmichTags, - JobStatus, - WithoutProperty, -} from '../repositories'; -import { MetadataService, Orientation } from './metadata.service'; +import { AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { MetadataService, Orientation } from 'src/services/metadata.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { fileStub } from 'test/fixtures/file.stub'; +import { probeStub } from 'test/fixtures/media.stub'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; +import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; +import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; describe(MetadataService.name, () => { let albumMock: jest.Mocked; @@ -53,7 +46,7 @@ describe(MetadataService.name, () => { let mediaMock: jest.Mocked; let personMock: jest.Mocked; let storageMock: jest.Mocked; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; let databaseMock: jest.Mocked; let sut: MetadataService; @@ -66,7 +59,7 @@ describe(MetadataService.name, () => { metadataMock = newMetadataRepositoryMock(); moveMock = newMoveRepositoryMock(); personMock = newPersonRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); + eventMock = newEventRepositoryMock(); storageMock = newStorageRepositoryMock(); mediaMock = newMediaRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); @@ -74,7 +67,7 @@ describe(MetadataService.name, () => { sut = new MetadataService( albumMock, assetMock, - communicationMock, + eventMock, cryptoRepository, databaseMock, jobMock, @@ -117,7 +110,7 @@ describe(MetadataService.name, () => { await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); expect(albumMock.removeAsset).not.toHaveBeenCalled(); }); @@ -127,7 +120,7 @@ describe(MetadataService.name, () => { await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); expect(albumMock.removeAsset).not.toHaveBeenCalled(); }); @@ -137,7 +130,7 @@ describe(MetadataService.name, () => { await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); expect(albumMock.removeAsset).not.toHaveBeenCalled(); }); @@ -159,7 +152,7 @@ describe(MetadataService.name, () => { otherAssetId: assetStub.livePhotoMotionAsset.id, type: AssetType.IMAGE, }); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); expect(albumMock.removeAsset).not.toHaveBeenCalled(); }); @@ -182,11 +175,11 @@ describe(MetadataService.name, () => { otherAssetId: assetStub.livePhotoStillAsset.id, type: AssetType.VIDEO, }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoStillAsset.id, livePhotoVideoId: assetStub.livePhotoMotionAsset.id, }); - expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); }); @@ -202,7 +195,7 @@ describe(MetadataService.name, () => { await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( JobStatus.SUCCESS, ); - expect(communicationMock.send).toHaveBeenCalledWith( + expect(eventMock.clientSend).toHaveBeenCalledWith( ClientEvent.ASSET_HIDDEN, assetStub.livePhotoMotionAsset.ownerId, assetStub.livePhotoMotionAsset.id, @@ -248,7 +241,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should handle a date in a sidecar file', async () => { @@ -267,7 +260,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]); expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate })); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, duration: null, fileCreatedAt: sidecarDate, @@ -282,7 +275,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 })); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, duration: null, fileCreatedAt: assetStub.image.createdAt, @@ -304,7 +297,7 @@ describe(MetadataService.name, () => { expect(assetMock.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }), ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.withLocation.id, duration: null, fileCreatedAt: assetStub.withLocation.createdAt, @@ -333,7 +326,7 @@ describe(MetadataService.name, () => { expect(storageMock.writeFile).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalledWith( + expect(assetMock.update).not.toHaveBeenCalledWith( expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }), ); }); @@ -376,7 +369,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); - expect(assetMock.save).toHaveBeenNthCalledWith(1, { + expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoStillAsset.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); @@ -404,7 +397,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); - expect(assetMock.save).toHaveBeenNthCalledWith(1, { + expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoStillAsset.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); @@ -430,7 +423,7 @@ describe(MetadataService.name, () => { expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object)); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); - expect(assetMock.save).toHaveBeenNthCalledWith(1, { + expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoStillAsset.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); @@ -470,7 +463,7 @@ describe(MetadataService.name, () => { expect(assetMock.create).toHaveBeenCalledTimes(0); expect(storageMock.writeFile).toHaveBeenCalledTimes(0); // The still asset gets saved by handleMetadataExtraction, but not the video - expect(assetMock.save).toHaveBeenCalledTimes(1); + expect(assetMock.update).toHaveBeenCalledTimes(1); expect(jobMock.queue).toHaveBeenCalledTimes(0); }); @@ -529,7 +522,7 @@ describe(MetadataService.name, () => { projectionType: 'EQUIRECTANGULAR', timeZone: tags.tz, }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, duration: null, fileCreatedAt: new Date('1970-01-01'), @@ -545,7 +538,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); - expect(assetMock.save).toHaveBeenCalledWith( + expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, duration: '00:00:06.210', @@ -561,7 +554,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); - expect(assetMock.save).toHaveBeenCalledWith( + expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, duration: '00:00:08.410', @@ -577,7 +570,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); - expect(assetMock.save).toHaveBeenCalledWith( + expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, duration: '00:00:06.200', @@ -593,7 +586,7 @@ describe(MetadataService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); - expect(assetMock.save).toHaveBeenCalledWith( + expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, duration: '00:00:06.207', @@ -638,13 +631,13 @@ describe(MetadataService.name, () => { it('should do nothing if asset could not be found', async () => { assetMock.getByIds.mockResolvedValue([]); await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should do nothing if asset has no sidecar path', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => { @@ -653,7 +646,7 @@ describe(MetadataService.name, () => { await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.sidecar.id, sidecarPath: assetStub.sidecar.sidecarPath, }); @@ -670,7 +663,7 @@ describe(MetadataService.name, () => { assetStub.sidecarWithoutExt.sidecarPath, constants.R_OK, ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.sidecarWithoutExt.id, sidecarPath: assetStub.sidecarWithoutExt.sidecarPath, }); @@ -688,7 +681,7 @@ describe(MetadataService.name, () => { assetStub.sidecarWithoutExt.sidecarPath, constants.R_OK, ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.sidecar.id, sidecarPath: assetStub.sidecar.sidecarPath, }); @@ -700,7 +693,7 @@ describe(MetadataService.name, () => { await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.sidecar.id, sidecarPath: null, }); @@ -724,16 +717,15 @@ describe(MetadataService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); storageMock.checkFileExists.mockResolvedValue(false); await sut.handleSidecarDiscovery({ id: assetStub.image.id }); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should update a image asset when a sidecar is found', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - assetMock.save.mockResolvedValue(assetStub.image); storageMock.checkFileExists.mockResolvedValue(true); await sut.handleSidecarDiscovery({ id: assetStub.image.id }); expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, sidecarPath: '/original/path.jpg.xmp', }); @@ -741,11 +733,10 @@ describe(MetadataService.name, () => { it('should update a video asset when a sidecar is found', async () => { assetMock.getByIds.mockResolvedValue([assetStub.video]); - assetMock.save.mockResolvedValue(assetStub.video); storageMock.checkFileExists.mockResolvedValue(true); await sut.handleSidecarDiscovery({ id: assetStub.video.id }); expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, sidecarPath: '/original/path.ext.xmp', }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/services/metadata.service.ts similarity index 90% rename from server/src/domain/metadata/metadata.service.ts rename to server/src/services/metadata.service.ts index 5f0b28fc4..9b249a49f 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -1,5 +1,3 @@ -import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { ExifDateTime, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; @@ -8,29 +6,34 @@ import { Duration } from 'luxon'; import { constants } from 'node:fs/promises'; import path from 'node:path'; import { Subscription } from 'rxjs'; -import { handlePromiseError, usePagination } from '../domain.util'; -import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; +import { StorageCore } from 'src/cores/storage.core'; +import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { - ClientEvent, - DatabaseLock, - IAlbumRepository, - IAssetRepository, - ICommunicationRepository, - ICryptoRepository, - IDatabaseRepository, + IBaseJob, + IEntityJob, IJobRepository, - IMediaRepository, - IMetadataRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, - ImmichTags, + ISidecarWriteJob, + JOBS_ASSET_PAGINATION_SIZE, + JobName, JobStatus, - WithoutProperty, -} from '../repositories'; -import { StorageCore } from '../storage'; -import { FeatureFlag, SystemConfigCore } from '../system-config'; + QueueName, +} from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { handlePromiseError } from 'src/utils/misc'; +import { usePagination } from 'src/utils/pagination'; /** look for a date from these tags (in order) */ const EXIF_DATE_TAGS: Array = [ @@ -102,7 +105,7 @@ export class MetadataService { constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @@ -177,12 +180,12 @@ export class MetadataService { const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; - await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }); - await this.assetRepository.save({ id: motionAsset.id, isVisible: false }); + await this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }); + await this.assetRepository.update({ id: motionAsset.id, isVisible: false }); await this.albumRepository.removeAsset(motionAsset.id); // Notify clients to hide the linked live photo asset - this.communicationRepository.send(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id); + this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id); return JobStatus.SUCCESS; } @@ -249,7 +252,7 @@ export class MetadataService { if (dateTimeOriginal && timeZoneOffset) { localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000); } - await this.assetRepository.save({ + await this.assetRepository.update({ id: asset.id, duration: tags.Duration ? this.getDuration(tags.Duration) : null, localDateTime, @@ -317,7 +320,7 @@ export class MetadataService { await this.repository.writeTags(sidecarPath, exif); if (!asset.sidecarPath) { - await this.assetRepository.save({ id, sidecarPath }); + await this.assetRepository.update({ id, sidecarPath }); } return JobStatus.SUCCESS; @@ -435,7 +438,7 @@ export class MetadataService { this.storageCore.ensureFolders(motionPath); await this.storageRepository.writeFile(motionAsset.originalPath, video); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } }); - await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id }); + await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id }); // If the asset already had an associated livePhotoVideo, delete it, because // its checksum doesn't match the checksum of the motionAsset we just extracted @@ -587,7 +590,7 @@ export class MetadataService { } if (sidecarPath) { - await this.assetRepository.save({ id: asset.id, sidecarPath }); + await this.assetRepository.update({ id: asset.id, sidecarPath }); return JobStatus.SUCCESS; } @@ -598,7 +601,7 @@ export class MetadataService { this.logger.debug( `Sidecar file was not found. Checked paths '${sidecarPathWithExt}' and '${sidecarPathWithoutExt}'. Removing sidecarPath for asset ${asset.id}`, ); - await this.assetRepository.save({ id: asset.id, sidecarPath: null }); + await this.assetRepository.update({ id: asset.id, sidecarPath: null }); return JobStatus.SUCCESS; } diff --git a/server/src/microservices/app.service.ts b/server/src/services/microservices.service.ts similarity index 77% rename from server/src/microservices/app.service.ts rename to server/src/services/microservices.service.ts index 9fabd5855..7bea8c377 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/services/microservices.service.ts @@ -1,25 +1,22 @@ -import { - AssetService, - AuditService, - DatabaseService, - IDeleteFilesJob, - JobName, - JobService, - LibraryService, - MediaService, - MetadataService, - PersonService, - SmartInfoService, - StorageService, - StorageTemplateService, - SystemConfigService, - UserService, -} from '@app/domain'; -import { otelSDK } from '@app/infra/instrumentation'; import { Injectable } from '@nestjs/common'; +import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface'; +import { AssetService } from 'src/services/asset.service'; +import { AuditService } from 'src/services/audit.service'; +import { DatabaseService } from 'src/services/database.service'; +import { JobService } from 'src/services/job.service'; +import { LibraryService } from 'src/services/library.service'; +import { MediaService } from 'src/services/media.service'; +import { MetadataService } from 'src/services/metadata.service'; +import { PersonService } from 'src/services/person.service'; +import { SmartInfoService } from 'src/services/smart-info.service'; +import { StorageTemplateService } from 'src/services/storage-template.service'; +import { StorageService } from 'src/services/storage.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { UserService } from 'src/services/user.service'; +import { otelSDK } from 'src/utils/instrumentation'; @Injectable() -export class AppService { +export class MicroservicesService { constructor( private auditService: AuditService, private assetService: AssetService, @@ -56,9 +53,9 @@ export class AppService { [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data), [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data), [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data), - [JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data), - [JobName.GENERATE_WEBP_THUMBNAIL]: (data) => this.mediaService.handleGenerateWebpThumbnail(data), - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbhashThumbnail(data), + [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data), + [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data), + [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data), [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data), [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data), diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/services/partner.service.spec.ts similarity index 87% rename from server/src/domain/partner/partner.service.spec.ts rename to server/src/services/partner.service.spec.ts index 6e9c10c5c..a3c4af736 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -1,9 +1,12 @@ -import { UserAvatarColor } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; -import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; -import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories'; -import { PartnerResponseDto } from './partner.dto'; -import { PartnerService } from './partner.service'; +import { PartnerResponseDto } from 'src/dtos/partner.dto'; +import { UserAvatarColor } from 'src/entities/user.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface'; +import { PartnerService } from 'src/services/partner.service'; +import { authStub } from 'test/fixtures/auth.stub'; +import { partnerStub } from 'test/fixtures/partner.stub'; +import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; const responseDto = { admin: { diff --git a/server/src/domain/partner/partner.service.ts b/server/src/services/partner.service.ts similarity index 84% rename from server/src/domain/partner/partner.service.ts rename to server/src/services/partner.service.ts index a3f9a9f3d..14503cc7f 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,10 +1,11 @@ -import { PartnerEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from '../access'; -import { AuthDto } from '../auth'; -import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; -import { mapUser } from '../user'; -import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { mapUser } from 'src/dtos/user.dto'; +import { PartnerEntity } from 'src/entities/partner.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; @Injectable() export class PartnerService { diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/services/person.service.spec.ts similarity index 94% rename from server/src/domain/person/person.service.spec.ts rename to server/src/services/person.service.spec.ts index 08b5875a5..501154c1d 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,44 +1,36 @@ -import { AssetFaceEntity, Colorspace, SystemConfigKey } from '@app/infra/entities'; import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - assetStub, - authStub, - faceStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newCryptoRepositoryMock, - newJobRepositoryMock, - newMachineLearningRepositoryMock, - newMediaRepositoryMock, - newMoveRepositoryMock, - newPersonRepositoryMock, - newSearchRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - personStub, -} from '@test'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { Colorspace, SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { PersonService } from 'src/services/person.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { faceStub } from 'test/fixtures/face.stub'; +import { personStub } from 'test/fixtures/person.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; +import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; +import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; import { IsNull } from 'typeorm'; -import { BulkIdErrorReason } from '../asset'; -import { CacheControl, ImmichFileResponse } from '../domain.util'; -import { JobName } from '../job'; -import { - FaceSearchResult, - IAssetRepository, - ICryptoRepository, - IJobRepository, - IMachineLearningRepository, - IMediaRepository, - IMoveRepository, - IPersonRepository, - ISearchRepository, - IStorageRepository, - ISystemConfigRepository, - JobStatus, - WithoutProperty, -} from '../repositories'; -import { PersonResponseDto, mapFaces, mapPerson } from './person.dto'; -import { PersonService } from './person.service'; const responseDto: PersonResponseDto = { id: 'person-1', @@ -653,7 +645,7 @@ describe(PersonService.name, () => { expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', { - imagePath: assetStub.image.resizePath, + imagePath: assetStub.image.previewPath, }, { enabled: true, diff --git a/server/src/domain/person/person.service.ts b/server/src/services/person.service.ts similarity index 90% rename from server/src/domain/person/person.service.ts rename to server/src/services/person.service.ts index 1a2233f3c..d2bc81b0e 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,35 +1,11 @@ -import { PersonEntity } from '@app/infra/entities'; -import { PersonPathType } from '@app/infra/entities/move.entity'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { IsNull } from 'typeorm'; -import { AccessCore, Permission } from '../access'; -import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset'; -import { AuthDto } from '../auth'; -import { mimeTypes } from '../domain.constant'; -import { CacheControl, ImmichFileResponse, usePagination } from '../domain.util'; -import { IBaseJob, IDeferrableJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; -import { FACE_THUMBNAIL_SIZE } from '../media'; -import { - CropOptions, - IAccessRepository, - IAssetRepository, - ICryptoRepository, - IJobRepository, - IMachineLearningRepository, - IMediaRepository, - IMoveRepository, - IPersonRepository, - ISearchRepository, - IStorageRepository, - ISystemConfigRepository, - JobItem, - JobStatus, - UpdateFacesData, - WithoutProperty, -} from '../repositories'; -import { StorageCore } from '../storage'; -import { SystemConfigCore } from '../system-config'; +import { FACE_THUMBNAIL_SIZE } from 'src/constants'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { StorageCore } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceResponseDto, AssetFaceUpdateDto, @@ -44,7 +20,36 @@ import { PersonUpdateDto, mapFaces, mapPerson, -} from './person.dto'; +} from 'src/dtos/person.dto'; +import { PersonPathType } from 'src/entities/move.entity'; +import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { + IBaseJob, + IDeferrableJob, + IEntityJob, + IJobRepository, + JOBS_ASSET_PAGINATION_SIZE, + JobItem, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { CropOptions, IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; +import { IsNull } from 'typeorm'; @Injectable() export class PersonService { @@ -64,7 +69,7 @@ export class PersonService { @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, ) { this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(configRepository); @@ -311,17 +316,17 @@ export class PersonService { }, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.resizePath || asset.faces?.length > 0) { + if (!asset || !asset.previewPath || asset.faces?.length > 0) { return JobStatus.FAILED; } const faces = await this.machineLearningRepository.detectFaces( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); + this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` }))); if (faces.length > 0) { @@ -466,7 +471,7 @@ export class PersonService { } async handleGeneratePersonThumbnail(data: IEntityJob): Promise { - const { machineLearning, thumbnail } = await this.configCore.getConfig(); + const { machineLearning, image } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { return JobStatus.SKIPPED; } @@ -492,7 +497,7 @@ export class PersonService { } = face; const [asset] = await this.assetRepository.getByIds([assetId]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } this.logger.verbose(`Cropping face for person: ${person.id}`); @@ -523,12 +528,12 @@ export class PersonService { height: newHalfSize * 2, }; - const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions); + const croppedOutput = await this.mediaRepository.crop(asset.previewPath, cropOptions); const thumbnailOptions = { - format: 'jpeg', + format: ImageFormat.JPEG, size: FACE_THUMBNAIL_SIZE, - colorspace: thumbnail.colorspace, - quality: thumbnail.quality, + colorspace: image.colorspace, + quality: image.quality, } as const; await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/services/search.service.spec.ts similarity index 80% rename from server/src/domain/search/search.service.spec.ts rename to server/src/services/search.service.spec.ts index b6edf1ece..72b543f2d 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -1,28 +1,24 @@ -import { SystemConfigKey } from '@app/infra/entities'; -import { - assetStub, - authStub, - newAssetRepositoryMock, - newMachineLearningRepositoryMock, - newMetadataRepositoryMock, - newPartnerRepositoryMock, - newPersonRepositoryMock, - newSearchRepositoryMock, - newSystemConfigRepositoryMock, - personStub, -} from '@test'; -import { mapAsset } from '../asset'; -import { - IAssetRepository, - IMachineLearningRepository, - IMetadataRepository, - IPartnerRepository, - IPersonRepository, - ISearchRepository, - ISystemConfigRepository, -} from '../repositories'; -import { SearchDto } from './dto'; -import { SearchService } from './search.service'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { SearchDto } from 'src/dtos/search.dto'; +import { SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SearchService } from 'src/services/search.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { personStub } from 'test/fixtures/person.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; +import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; +import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; jest.useFakeTimers(); diff --git a/server/src/domain/search/search.service.ts b/server/src/services/search.service.ts similarity index 82% rename from server/src/domain/search/search.service.ts rename to server/src/services/search.service.ts index 56c4498bc..9422dac86 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,31 +1,29 @@ -import { AssetEntity, AssetOrder } from '@app/infra/entities'; import { Inject, Injectable } from '@nestjs/common'; -import { AssetResponseDto, mapAsset } from '../asset'; -import { AuthDto } from '../auth'; -import { PersonResponseDto } from '../person'; -import { - IAssetRepository, - IMachineLearningRepository, - IMetadataRepository, - IPartnerRepository, - IPersonRepository, - ISearchRepository, - ISystemConfigRepository, - SearchExploreItem, - SearchStrategy, -} from '../repositories'; -import { FeatureFlag, SystemConfigCore } from '../system-config'; +import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, SearchDto, SearchPeopleDto, SearchPlacesDto, + SearchResponseDto, + SearchSuggestionRequestDto, + SearchSuggestionType, SmartSearchDto, mapPlaces, -} from './dto'; -import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto'; -import { SearchResponseDto } from './response-dto'; +} from 'src/dtos/search.dto'; +import { AssetOrder } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; @Injectable() export class SearchService { @@ -78,6 +76,9 @@ export class SearchService { checksum = Buffer.from(dto.checksum, encoding); } + dto.previewPath ??= dto.resizePath; + dto.thumbnailPath ??= dto.webpPath; + const page = dto.page ?? 1; const size = dto.size || 250; const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; @@ -115,6 +116,32 @@ export class SearchService { return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); } + async getAssetsByCity(auth: AuthDto): Promise { + const userIds = await this.getUserIdsToSearch(auth); + const assets = await this.searchRepository.getAssetsByCity(userIds); + return assets.map((asset) => mapAsset(asset)); + } + + getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise { + switch (dto.type) { + case SearchSuggestionType.COUNTRY: { + return this.metadataRepository.getCountries(auth.user.id); + } + case SearchSuggestionType.STATE: { + return this.metadataRepository.getStates(auth.user.id, dto.country); + } + case SearchSuggestionType.CITY: { + return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); + } + case SearchSuggestionType.CAMERA_MAKE: { + return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); + } + case SearchSuggestionType.CAMERA_MODEL: { + return this.metadataRepository.getCameraModels(auth.user.id, dto.make); + } + } + } + // TODO: remove after implementing new search filters /** @deprecated */ async search(auth: AuthDto, dto: SearchDto): Promise { @@ -191,24 +218,4 @@ export class SearchService { }, }; } - - async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise { - switch (dto.type) { - case SearchSuggestionType.COUNTRY: { - return this.metadataRepository.getCountries(auth.user.id); - } - case SearchSuggestionType.STATE: { - return this.metadataRepository.getStates(auth.user.id, dto.country); - } - case SearchSuggestionType.CITY: { - return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); - } - case SearchSuggestionType.CAMERA_MAKE: { - return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); - } - case SearchSuggestionType.CAMERA_MODEL: { - return this.metadataRepository.getCameraModels(auth.user.id, dto.make); - } - } - } } diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts similarity index 84% rename from server/src/domain/server-info/server-info.service.spec.ts rename to server/src/services/server-info.service.spec.ts index 8c90f8107..0348f26d2 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,26 +1,22 @@ -import { SystemMetadataKey } from '@app/infra/entities'; -import { - newCommunicationRepositoryMock, - newServerInfoRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - newSystemMetadataRepositoryMock, - newUserRepositoryMock, -} from '@test'; -import { serverVersion } from '../domain.constant'; -import { - ICommunicationRepository, - IServerInfoRepository, - IStorageRepository, - ISystemConfigRepository, - ISystemMetadataRepository, - IUserRepository, -} from '../repositories'; -import { ServerInfoService } from './server-info.service'; +import { serverVersion } from 'src/constants'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { ServerInfoService } from 'src/services/server-info.service'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; +import { newServerInfoRepositoryMock } from 'test/repositories/system-info.repository.mock'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; describe(ServerInfoService.name, () => { let sut: ServerInfoService; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; let configMock: jest.Mocked; let serverInfoMock: jest.Mocked; let storageMock: jest.Mocked; @@ -29,20 +25,13 @@ describe(ServerInfoService.name, () => { beforeEach(() => { configMock = newSystemConfigRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); + eventMock = newEventRepositoryMock(); serverInfoMock = newServerInfoRepositoryMock(); storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); systemMetadataMock = newSystemMetadataRepositoryMock(); - sut = new ServerInfoService( - communicationMock, - configMock, - userMock, - serverInfoMock, - storageMock, - systemMetadataMock, - ); + sut = new ServerInfoService(eventMock, configMock, userMock, serverInfoMock, storageMock, systemMetadataMock); }); it('should work', () => { diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/services/server-info.service.ts similarity index 78% rename from server/src/domain/server-info/server-info.service.ts rename to server/src/services/server-info.service.ts index 04b3c4b6e..9f0c1e290 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -1,21 +1,9 @@ -import { SystemMetadataKey } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { Version, isDev, mimeTypes, serverVersion } from '../domain.constant'; -import { asHumanReadable } from '../domain.util'; -import { - ClientEvent, - ICommunicationRepository, - IServerInfoRepository, - IStorageRepository, - ISystemConfigRepository, - ISystemMetadataRepository, - IUserRepository, - UserStatsQueryResponse, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; -import { SystemConfigCore } from '../system-config'; +import { isDev, serverVersion } from 'src/constants'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnServerEvent } from 'src/decorators'; import { ServerConfigDto, ServerFeaturesDto, @@ -24,7 +12,18 @@ import { ServerPingResponse, ServerStatsResponseDto, UsageByUserDto, -} from './server-info.dto'; +} from 'src/dtos/server-info.dto'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface'; +import { asHumanReadable } from 'src/utils/bytes'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { Version } from 'src/utils/version'; @Injectable() export class ServerInfoService { @@ -34,7 +33,7 @@ export class ServerInfoService { private releaseVersionCheckedAt: DateTime | null = null; constructor( - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IServerInfoRepository) private repository: IServerInfoRepository, @@ -42,9 +41,10 @@ export class ServerInfoService { @Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository, ) { this.configCore = SystemConfigCore.create(configRepository); - this.communicationRepository.on('connect', (userId) => this.handleConnect(userId)); } + onConnect() {} + async init(): Promise { await this.handleVersionCheck(); @@ -171,8 +171,9 @@ export class ServerInfoService { return true; } - private handleConnect(userId: string) { - this.communicationRepository.send(ClientEvent.SERVER_VERSION, userId, serverVersion); + @OnServerEvent(ServerEvent.WEBSOCKET_CONNECT) + onWebsocketConnection({ userId }: ServerEventMap[ServerEvent.WEBSOCKET_CONNECT]) { + this.eventRepository.clientSend(ClientEvent.SERVER_VERSION, userId, serverVersion); this.newReleaseNotification(userId); } @@ -186,7 +187,7 @@ export class ServerInfoService { }; userId - ? this.communicationRepository.send(event, userId, payload) - : this.communicationRepository.broadcast(event, payload); + ? this.eventRepository.clientSend(event, userId, payload) + : this.eventRepository.clientBroadcast(event, payload); } } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts similarity index 92% rename from server/src/domain/shared-link/shared-link.service.spec.ts rename to server/src/services/shared-link.service.spec.ts index f0d0715a3..cad52928c 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -1,20 +1,17 @@ -import { SharedLinkType } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - albumStub, - assetStub, - authStub, - newAccessRepositoryMock, - newCryptoRepositoryMock, - newSharedLinkRepositoryMock, - sharedLinkResponseStub, - sharedLinkStub, -} from '@test'; import _ from 'lodash'; -import { AssetIdErrorReason } from '../asset'; -import { ICryptoRepository, ISharedLinkRepository } from '../repositories'; -import { SharedLinkService } from './shared-link.service'; +import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { SharedLinkType } from 'src/entities/shared-link.entity'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import { albumStub } from 'test/fixtures/album.stub'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; describe(SharedLinkService.name, () => { let sut: SharedLinkService; diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/services/shared-link.service.ts similarity index 89% rename from server/src/domain/shared-link/shared-link.service.ts rename to server/src/services/shared-link.service.ts index 54e6f6052..cea0e8414 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,12 +1,22 @@ -import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; -import { AccessCore, Permission } from '../access'; -import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; -import { AuthDto } from '../auth'; -import { OpenGraphTags } from '../domain.util'; -import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories'; -import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto'; -import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + SharedLinkCreateDto, + SharedLinkEditDto, + SharedLinkPasswordDto, + SharedLinkResponseDto, + mapSharedLink, + mapSharedLinkWithoutMetadata, +} from 'src/dtos/shared-link.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService { diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts similarity index 69% rename from server/src/domain/smart-info/smart-info.service.spec.ts rename to server/src/services/smart-info.service.spec.ts index 712c2b6a7..2e1dfbafc 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,29 +1,24 @@ -import { AssetEntity, SystemConfigKey } from '@app/infra/entities'; -import { - assetStub, - newAssetRepositoryMock, - newDatabaseRepositoryMock, - newJobRepositoryMock, - newMachineLearningRepositoryMock, - newSearchRepositoryMock, - newSystemConfigRepositoryMock, -} from '@test'; -import { JobName } from '../job'; -import { - IAssetRepository, - IDatabaseRepository, - IJobRepository, - IMachineLearningRepository, - ISearchRepository, - ISystemConfigRepository, - WithoutProperty, -} from '../repositories'; -import { cleanModelName, getCLIPModelInfo } from './smart-info.constant'; -import { SmartInfoService } from './smart-info.service'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SmartInfoService } from 'src/services/smart-info.service'; +import { getCLIPModelInfo } from 'src/utils/misc'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; +import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; const asset = { id: 'asset-1', - resizePath: 'path/to/resize.ext', + previewPath: 'path/to/resize.ext', } as AssetEntity; describe(SmartInfoService.name, () => { @@ -99,7 +94,7 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - const asset = { resizePath: '' } as AssetEntity; + const asset = { previewPath: '' } as AssetEntity; assetMock.getByIds.mockResolvedValue([asset]); await sut.handleEncodeClip({ id: asset.id }); @@ -109,7 +104,6 @@ describe(SmartInfoService.name, () => { }); it('should save the returned objects', async () => { - searchMock.upsert.mockResolvedValue(); machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); await sut.handleEncodeClip({ id: asset.id }); @@ -119,25 +113,18 @@ describe(SmartInfoService.name, () => { { imagePath: 'path/to/resize.ext' }, { enabled: true, modelName: 'ViT-B-32__openai' }, ); - expect(searchMock.upsert).toHaveBeenCalledWith( - { - assetId: 'asset-1', - }, - [0.01, 0.02, 0.03], - ); - }); - }); - - describe('cleanModelName', () => { - it('should clean name', () => { - expect(cleanModelName('ViT-B-32::openai')).toEqual('ViT-B-32__openai'); - expect(cleanModelName('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual('XLM-Roberta-Large-Vit-L-14'); + expect(searchMock.upsert).toHaveBeenCalledWith('asset-1', [0.01, 0.02, 0.03]); }); }); describe('getCLIPModelInfo', () => { it('should return the model info', () => { expect(getCLIPModelInfo('ViT-B-32__openai')).toEqual({ dimSize: 512 }); + expect(getCLIPModelInfo('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual({ dimSize: 768 }); + }); + + it('should clean the model name', () => { + expect(getCLIPModelInfo('ViT-B-32::openai')).toEqual({ dimSize: 512 }); }); it('should throw an error if the model is not present', () => { diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/services/smart-info.service.ts similarity index 77% rename from server/src/domain/smart-info/smart-info.service.ts rename to server/src/services/smart-info.service.ts index b7dd1a91f..f9d36c238 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,19 +1,21 @@ -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { usePagination } from '../domain.util'; -import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { - DatabaseLock, - IAssetRepository, - IDatabaseRepository, + IBaseJob, + IEntityJob, IJobRepository, - IMachineLearningRepository, - ISearchRepository, - ISystemConfigRepository, + JOBS_ASSET_PAGINATION_SIZE, + JobName, JobStatus, - WithoutProperty, -} from '../repositories'; -import { SystemConfigCore } from '../system-config'; + QueueName, +} from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class SmartInfoService { @@ -81,13 +83,13 @@ export class SmartInfoService { return JobStatus.FAILED; } - if (!asset.resizePath) { + if (!asset.previewPath) { return JobStatus.FAILED; } const clipEmbedding = await this.machineLearning.encodeImage( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.clip, ); @@ -96,7 +98,7 @@ export class SmartInfoService { await this.databaseRepository.wait(DatabaseLock.CLIPDimSize); } - await this.repository.upsert({ assetId: asset.id }, clipEmbedding); + await this.repository.upsert(asset.id, clipEmbedding); return JobStatus.SUCCESS; } diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts similarity index 89% rename from server/src/domain/storage-template/storage-template.service.spec.ts rename to server/src/services/storage-template.service.spec.ts index 21fa6ef7d..ba1cb3e59 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,34 +1,30 @@ -import { - IAlbumRepository, - IAssetRepository, - ICryptoRepository, - IDatabaseRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, - JobStatus, - StorageTemplateService, - defaults, -} from '@app/domain'; -import { AssetPathType, SystemConfig, SystemConfigKey } from '@app/infra/entities'; -import { - assetStub, - newAlbumRepositoryMock, - newAssetRepositoryMock, - newCryptoRepositoryMock, - newDatabaseRepositoryMock, - newMoveRepositoryMock, - newPersonRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - newUserRepositoryMock, - userStub, -} from '@test'; import { when } from 'jest-when'; import { Stats } from 'node:fs'; -import { SystemConfigCore } from '../system-config'; +import { SystemConfigCore, defaults } from 'src/cores/system-config.core'; +import { AssetPathType } from 'src/entities/move.entity'; +import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { StorageTemplateService } from 'src/services/storage-template.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; +import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; describe(StorageTemplateService.name, () => { let sut: StorageTemplateService; @@ -74,10 +70,10 @@ describe(StorageTemplateService.name, () => { SystemConfigCore.create(configMock).config$.next(defaults); }); - describe('validate', () => { + describe('onValidateConfig', () => { it('should allow valid templates', () => { expect(() => - sut.validate({ + sut.onValidateConfig({ newConfig: { storageTemplate: { template: @@ -91,7 +87,7 @@ describe(StorageTemplateService.name, () => { it('should fail for an invalid template', () => { expect(() => - sut.validate({ + sut.onValidateConfig({ newConfig: { storageTemplate: { template: '{{foo}}', @@ -111,7 +107,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.checkFileExists).not.toHaveBeenCalled(); expect(storageMock.rename).not.toHaveBeenCalled(); expect(storageMock.copyFile).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); expect(moveMock.create).not.toHaveBeenCalled(); expect(moveMock.update).not.toHaveBeenCalled(); expect(storageMock.stat).not.toHaveBeenCalled(); @@ -122,14 +118,6 @@ describe(StorageTemplateService.name, () => { const newMotionPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.mp4`; const newStillPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.jpeg`; - when(assetMock.save) - .calledWith({ id: assetStub.livePhotoStillAsset.id, originalPath: newStillPicturePath }) - .mockResolvedValue(assetStub.livePhotoStillAsset); - - when(assetMock.save) - .calledWith({ id: assetStub.livePhotoMotionAsset.id, originalPath: newMotionPicturePath }) - .mockResolvedValue(assetStub.livePhotoMotionAsset); - when(assetMock.getByIds) .calledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true }) .mockResolvedValue([assetStub.livePhotoStillAsset]); @@ -175,11 +163,11 @@ describe(StorageTemplateService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoStillAsset.id, originalPath: newStillPicturePath, }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, originalPath: newMotionPicturePath, }); @@ -200,10 +188,6 @@ describe(StorageTemplateService.name, () => { newPath: previousFailedNewPath, }); - when(assetMock.save) - .calledWith({ id: assetStub.image.id, originalPath: newPath }) - .mockResolvedValue(assetStub.image); - when(assetMock.getByIds) .calledWith([assetStub.image.id], { exifInfo: true }) .mockResolvedValue([assetStub.image]); @@ -232,7 +216,7 @@ describe(StorageTemplateService.name, () => { oldPath: assetStub.image.originalPath, newPath, }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: newPath, }); @@ -257,10 +241,6 @@ describe(StorageTemplateService.name, () => { newPath: previousFailedNewPath, }); - when(assetMock.save) - .calledWith({ id: assetStub.image.id, originalPath: newPath }) - .mockResolvedValue(assetStub.image); - when(assetMock.getByIds) .calledWith([assetStub.image.id], { exifInfo: true }) .mockResolvedValue([assetStub.image]); @@ -291,7 +271,7 @@ describe(StorageTemplateService.name, () => { oldPath: previousFailedNewPath, newPath, }); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: newPath, }); @@ -307,10 +287,6 @@ describe(StorageTemplateService.name, () => { .mockResolvedValue({ size: 5000 } as Stats); when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8')); - when(assetMock.save) - .calledWith({ id: assetStub.image.id, originalPath: newPath }) - .mockResolvedValue(assetStub.image); - when(assetMock.getByIds) .calledWith([assetStub.image.id], { exifInfo: true }) .mockResolvedValue([assetStub.image]); @@ -345,7 +321,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); expect(storageMock.unlink).toHaveBeenCalledWith(newPath); expect(storageMock.unlink).toHaveBeenCalledTimes(1); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it.each` @@ -374,10 +350,6 @@ describe(StorageTemplateService.name, () => { newPath: previousFailedNewPath, }); - when(assetMock.save) - .calledWith({ id: assetStub.image.id, originalPath: newPath }) - .mockResolvedValue(assetStub.image); - when(assetMock.getByIds) .calledWith([assetStub.image.id], { exifInfo: true }) .mockResolvedValue([assetStub.image]); @@ -404,7 +376,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.rename).not.toHaveBeenCalled(); expect(storageMock.copyFile).not.toHaveBeenCalled(); expect(moveMock.update).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }, ); }); @@ -427,7 +399,6 @@ describe(StorageTemplateService.name, () => { items: [assetStub.image], hasNextPage: false, }); - assetMock.save.mockResolvedValue(assetStub.image); userMock.getList.mockResolvedValue([userStub.user1]); moveMock.create.mockResolvedValue({ id: '123', @@ -449,7 +420,7 @@ describe(StorageTemplateService.name, () => { expect(assetMock.getAll).toHaveBeenCalled(); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg', }); @@ -474,7 +445,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.rename).not.toHaveBeenCalled(); expect(storageMock.copyFile).not.toHaveBeenCalled(); expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should skip when an asset is probably a duplicate', async () => { @@ -495,7 +466,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.rename).not.toHaveBeenCalled(); expect(storageMock.copyFile).not.toHaveBeenCalled(); expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should move an asset', async () => { @@ -503,7 +474,6 @@ describe(StorageTemplateService.name, () => { items: [assetStub.image], hasNextPage: false, }); - assetMock.save.mockResolvedValue(assetStub.image); userMock.getList.mockResolvedValue([userStub.user1]); moveMock.create.mockResolvedValue({ id: '123', @@ -520,7 +490,7 @@ describe(StorageTemplateService.name, () => { '/original/path.jpg', 'upload/library/user-id/2023/2023-02-23/asset-id.jpg', ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg', }); @@ -531,7 +501,6 @@ describe(StorageTemplateService.name, () => { items: [assetStub.image], hasNextPage: false, }); - assetMock.save.mockResolvedValue(assetStub.image); userMock.getList.mockResolvedValue([userStub.storageLabel]); moveMock.create.mockResolvedValue({ id: '123', @@ -548,7 +517,7 @@ describe(StorageTemplateService.name, () => { '/original/path.jpg', 'upload/library/label-1/2023/2023-02-23/asset-id.jpg', ); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.jpg', }); @@ -592,7 +561,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date)); expect(storageMock.unlink).toHaveBeenCalledWith(assetStub.image.originalPath); expect(storageMock.unlink).toHaveBeenCalledTimes(1); - expect(assetMock.save).toHaveBeenCalledWith({ + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, originalPath: newPath, }); @@ -630,7 +599,7 @@ describe(StorageTemplateService.name, () => { 'upload/library/user-id/2023/2023-02-23/asset-id.jpg', ); expect(storageMock.stat).toHaveBeenCalledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg'); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should not update the database if the move fails', async () => { @@ -656,7 +625,7 @@ describe(StorageTemplateService.name, () => { '/original/path.jpg', 'upload/library/user-id/2023/2023-02-23/asset-id.jpg', ); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); it('should not move read-only asset', async () => { @@ -670,7 +639,6 @@ describe(StorageTemplateService.name, () => { ], hasNextPage: false, }); - assetMock.save.mockResolvedValue(assetStub.image); userMock.getList.mockResolvedValue([userStub.user1]); await sut.handleMigration(); @@ -678,7 +646,7 @@ describe(StorageTemplateService.name, () => { expect(assetMock.getAll).toHaveBeenCalled(); expect(storageMock.rename).not.toHaveBeenCalled(); expect(storageMock.copyFile).not.toHaveBeenCalled(); - expect(assetMock.save).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); }); }); diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/services/storage-template.service.ts similarity index 86% rename from server/src/domain/storage-template/storage-template.service.ts rename to server/src/services/storage-template.service.ts index ffdbfbefb..280c37b95 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -1,29 +1,8 @@ -import { AssetEntity, AssetPathType, AssetType, SystemConfig } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import handlebar from 'handlebars'; -import * as luxon from 'luxon'; +import { DateTime } from 'luxon'; import path from 'node:path'; import sanitize from 'sanitize-filename'; -import { getLivePhotoMotionFilename, usePagination } from '../domain.util'; -import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE } from '../job'; -import { - DatabaseLock, - IAlbumRepository, - IAssetRepository, - ICryptoRepository, - IDatabaseRepository, - IMoveRepository, - IPersonRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, - InternalEvent, - InternalEventMap, - JobStatus, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; import { supportedDayTokens, supportedHourTokens, @@ -32,8 +11,27 @@ import { supportedSecondTokens, supportedWeekTokens, supportedYearTokens, -} from '../system-config'; -import { SystemConfigCore } from '../system-config/system-config.core'; +} from 'src/constants'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnServerEvent } from 'src/decorators'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetPathType } from 'src/entities/move.entity'; +import { SystemConfig } from 'src/entities/system-config.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { ServerAsyncEvent, ServerAsyncEventMap } from 'src/interfaces/event.interface'; +import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { getLivePhotoMotionFilename } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; export interface MoveAssetMetadata { storageLabel: string | null; @@ -88,8 +86,8 @@ export class StorageTemplateService { ); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) - validate({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { + @OnServerEvent(ServerAsyncEvent.CONFIG_VALIDATE) + onValidateConfig({ newConfig }: ServerAsyncEventMap[ServerAsyncEvent.CONFIG_VALIDATE]) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); this.render(compiled, { @@ -312,7 +310,7 @@ export class StorageTemplateService { const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const zone = asset.exifInfo?.timeZone || systemTimeZone; - const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone }); + const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone }); const dateTokens = [ ...supportedYearTokens, diff --git a/server/src/domain/storage/storage.service.spec.ts b/server/src/services/storage.service.spec.ts similarity index 84% rename from server/src/domain/storage/storage.service.spec.ts rename to server/src/services/storage.service.spec.ts index 785891086..977f632d5 100644 --- a/server/src/domain/storage/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -1,6 +1,6 @@ -import { newStorageRepositoryMock } from '@test'; -import { IStorageRepository } from '../repositories'; -import { StorageService } from './storage.service'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { StorageService } from 'src/services/storage.service'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; describe(StorageService.name, () => { let sut: StorageService; diff --git a/server/src/domain/storage/storage.service.ts b/server/src/services/storage.service.ts similarity index 74% rename from server/src/domain/storage/storage.service.ts rename to server/src/services/storage.service.ts index 95c311881..81fdb4f41 100644 --- a/server/src/domain/storage/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,8 +1,8 @@ -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { IDeleteFilesJob } from '../job'; -import { IStorageRepository, JobStatus } from '../repositories'; -import { StorageCore, StorageFolder } from './storage.core'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class StorageService { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts similarity index 89% rename from server/src/domain/system-config/system-config.service.spec.ts rename to server/src/services/system-config.service.spec.ts index fd9c16463..4bb5dd0a1 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,7 +1,10 @@ +import { BadRequestException } from '@nestjs/common'; +import { defaults } from 'src/cores/system-config.core'; import { AudioCodec, - Colorspace, CQMode, + Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -10,14 +13,15 @@ import { TranscodeHWAccel, TranscodePolicy, VideoCodec, -} from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; -import { BadRequestException } from '@nestjs/common'; -import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test'; -import { QueueName } from '../job'; -import { ICommunicationRepository, ISearchRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; -import { defaults } from './system-config.core'; -import { SystemConfigService } from './system-config.service'; +} from 'src/entities/system-config.entity'; +import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; +import { QueueName } from 'src/interfaces/job.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { ImmichLogger } from 'src/utils/logger'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; const updates: SystemConfigEntity[] = [ { key: SystemConfigKey.FFMPEG_CRF, value: 30 }, @@ -116,9 +120,11 @@ const updatedConfig = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, @@ -149,14 +155,14 @@ const updatedConfig = Object.freeze({ describe(SystemConfigService.name, () => { let sut: SystemConfigService; let configMock: jest.Mocked; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; let smartInfoMock: jest.Mocked; beforeEach(() => { delete process.env.IMMICH_CONFIG_FILE; configMock = newSystemConfigRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); - sut = new SystemConfigService(configMock, communicationMock, smartInfoMock); + eventMock = newEventRepositoryMock(); + sut = new SystemConfigService(configMock, eventMock, smartInfoMock); }); it('should work', () => { @@ -327,8 +333,8 @@ describe(SystemConfigService.name, () => { await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig); - expect(communicationMock.broadcast).toHaveBeenCalled(); - expect(communicationMock.sendServerEvent).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE); + expect(eventMock.clientBroadcast).toHaveBeenCalled(); + expect(eventMock.serverSend).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE, null); expect(configMock.saveAll).toHaveBeenCalledWith(updates); }); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/services/system-config.service.ts similarity index 74% rename from server/src/domain/system-config/system-config.service.ts rename to server/src/services/system-config.service.ts index 7e68cf0b9..bc57c83ac 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,20 +1,6 @@ -import { LogLevel, SystemConfig } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; -import { - ClientEvent, - ICommunicationRepository, - ISearchRepository, - ISystemConfigRepository, - InternalEvent, - InternalEventMap, - ServerEvent, -} from '../repositories'; -import { SystemConfigDto, mapConfig } from './dto/system-config.dto'; -import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; import { supportedDayTokens, supportedHourTokens, @@ -24,8 +10,21 @@ import { supportedSecondTokens, supportedWeekTokens, supportedYearTokens, -} from './system-config.constants'; -import { SystemConfigCore } from './system-config.core'; +} from 'src/constants'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnServerEvent } from 'src/decorators'; +import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; +import { LogLevel, SystemConfig } from 'src/entities/system-config.entity'; +import { + ClientEvent, + IEventRepository, + ServerAsyncEvent, + ServerAsyncEventMap, + ServerEvent, +} from 'src/interfaces/event.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class SystemConfigService { @@ -34,11 +33,10 @@ export class SystemConfigService { constructor( @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository, - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, ) { this.core = SystemConfigCore.create(repository); - this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate()); this.core.config$.subscribe((config) => this.setLogLevel(config)); } @@ -61,8 +59,8 @@ export class SystemConfigService { return mapConfig(config); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) - validateConfig({ newConfig, oldConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { + @OnServerEvent(ServerAsyncEvent.CONFIG_VALIDATE) + onValidateConfig({ newConfig, oldConfig }: ServerAsyncEventMap[ServerAsyncEvent.CONFIG_VALIDATE]) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.'); } @@ -72,7 +70,10 @@ export class SystemConfigService { const oldConfig = await this.core.getConfig(); try { - await this.communicationRepository.emitAsync(InternalEvent.VALIDATE_CONFIG, { newConfig: dto, oldConfig }); + await this.eventRepository.serverSendAsync(ServerAsyncEvent.CONFIG_VALIDATE, { + newConfig: dto, + oldConfig, + }); } catch (error) { this.logger.warn(`Unable to save system config due to a validation error: ${error}`); throw new BadRequestException(error instanceof Error ? error.message : error); @@ -80,8 +81,8 @@ export class SystemConfigService { const newConfig = await this.core.updateConfig(dto); - this.communicationRepository.broadcast(ClientEvent.CONFIG_UPDATE, {}); - this.communicationRepository.sendServerEvent(ServerEvent.CONFIG_UPDATE); + this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); + this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null); if (oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { await this.smartInfoRepository.init(newConfig.machineLearning.clip.modelName); @@ -91,7 +92,7 @@ export class SystemConfigService { // this is only used by the cli on config change, and it's not actually needed anymore async refreshConfig() { - this.communicationRepository.sendServerEvent(ServerEvent.CONFIG_UPDATE); + this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null); await this.core.refreshConfig(); return true; } @@ -127,7 +128,8 @@ export class SystemConfigService { return theme.customCss; } - private async handleConfigUpdate() { + @OnServerEvent(ServerEvent.CONFIG_UPDATE) + async onConfigUpdate() { await this.core.refreshConfig(); } diff --git a/server/src/domain/tag/tag.service.spec.ts b/server/src/services/tag.service.spec.ts similarity index 93% rename from server/src/domain/tag/tag.service.spec.ts rename to server/src/services/tag.service.spec.ts index e987beb6a..2d684616a 100644 --- a/server/src/domain/tag/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -1,10 +1,13 @@ -import { TagType } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; -import { assetStub, authStub, newTagRepositoryMock, tagResponseStub, tagStub } from '@test'; import { when } from 'jest-when'; -import { AssetIdErrorReason } from '../asset'; -import { ITagRepository } from '../repositories'; -import { TagService } from './tag.service'; +import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { TagType } from 'src/entities/tag.entity'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { TagService } from 'src/services/tag.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; +import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; describe(TagService.name, () => { let sut: TagService; diff --git a/server/src/domain/tag/tag.service.ts b/server/src/services/tag.service.ts similarity index 88% rename from server/src/domain/tag/tag.service.ts rename to server/src/services/tag.service.ts index 38f1de1bc..c04f9b14c 100644 --- a/server/src/domain/tag/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,9 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset'; -import { AuthDto } from '../auth'; -import { ITagRepository } from '../repositories'; -import { TagResponseDto, mapTag } from './tag-response.dto'; -import { CreateTagDto, UpdateTagDto } from './tag.dto'; +import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateTagDto, TagResponseDto, UpdateTagDto, mapTag } from 'src/dtos/tag.dto'; +import { ITagRepository } from 'src/interfaces/tag.interface'; @Injectable() export class TagService { diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts new file mode 100644 index 000000000..c6f058022 --- /dev/null +++ b/server/src/services/timeline.service.spec.ts @@ -0,0 +1,149 @@ +import { BadRequestException } from '@nestjs/common'; +import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { TimelineService } from 'src/services/timeline.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; + +describe(TimelineService.name, () => { + let sut: TimelineService; + let accessMock: IAccessRepositoryMock; + let assetMock: jest.Mocked; + let partnerMock: jest.Mocked; + beforeEach(() => { + accessMock = newAccessRepositoryMock(); + assetMock = newAssetRepositoryMock(); + partnerMock = newPartnerRepositoryMock(); + + sut = new TimelineService(accessMock, assetMock, partnerMock); + }); + + describe('getTimeBuckets', () => { + it("should return buckets if userId and albumId aren't set", async () => { + assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]); + + await expect( + sut.getTimeBuckets(authStub.admin, { + size: TimeBucketSize.DAY, + }), + ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); + expect(assetMock.getTimeBuckets).toHaveBeenCalledWith({ + size: TimeBucketSize.DAY, + userIds: [authStub.admin.user.id], + }); + }); + }); + + describe('getTimeBucket', () => { + it('should return the assets for a album time bucket if user has album.read', async () => { + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); + + await expect( + sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), + ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id'])); + expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + albumId: 'album-id', + }); + }); + + it('should return the assets for a archive time bucket if user has archive.read', async () => { + assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: true, + userId: authStub.admin.user.id, + }), + ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: true, + userIds: [authStub.admin.user.id], + }); + }); + + it('should return the assets for a library time bucket if user has library.read', async () => { + assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + userId: authStub.admin.user.id, + }), + ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + userIds: [authStub.admin.user.id], + }); + }); + + it('should throw an error if withParners is true and isArchived true or undefined', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: true, + withPartners: true, + userId: authStub.admin.user.id, + }), + ).rejects.toThrow(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: undefined, + withPartners: true, + userId: authStub.admin.user.id, + }), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw an error if withParners is true and isFavorite is either true or false', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: true, + withPartners: true, + userId: authStub.admin.user.id, + }), + ).rejects.toThrow(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: false, + withPartners: true, + userId: authStub.admin.user.id, + }), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw an error if withParners is true and isTrash is true', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isTrashed: true, + withPartners: true, + userId: authStub.admin.user.id, + }), + ).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts new file mode 100644 index 000000000..95c4081e6 --- /dev/null +++ b/server/src/services/timeline.service.ts @@ -0,0 +1,86 @@ +import { BadRequestException, Inject } from '@nestjs/common'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; + +export class TimelineService { + private accessCore: AccessCore; + + constructor( + @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAssetRepository) private repository: IAssetRepository, + @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, + ) { + this.accessCore = AccessCore.create(accessRepository); + } + + async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { + await this.timeBucketChecks(auth, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); + + return this.repository.getTimeBuckets(timeBucketOptions); + } + + async getTimeBucket( + auth: AuthDto, + dto: TimeBucketAssetDto, + ): Promise { + await this.timeBucketChecks(auth, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); + const assets = await this.repository.getTimeBucket(dto.timeBucket, timeBucketOptions); + return !auth.sharedLink || auth.sharedLink?.showExif + ? assets.map((asset) => mapAsset(asset, { withStack: true, auth })) + : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth })); + } + + private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { + const { userId, ...options } = dto; + let userIds: string[] | undefined = undefined; + + if (userId) { + userIds = [userId]; + + if (dto.withPartners) { + const partners = await this.partnerRepository.getAll(auth.user.id); + const partnersIds = partners + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) + .map((partner) => partner.sharedById); + + userIds.push(...partnersIds); + } + } + + return { ...options, userIds }; + } + + private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { + if (dto.albumId) { + await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); + } else { + dto.userId = dto.userId || auth.user.id; + } + + if (dto.userId) { + await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); + if (dto.isArchived !== false) { + await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); + } + } + + if (dto.withPartners) { + const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; + const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; + const requestedTrash = dto.isTrashed === true; + + if (requestedArchived || requestedFavorite || requestedTrash) { + throw new BadRequestException( + 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + ); + } + } + } +} diff --git a/server/src/domain/trash/trash.service.spec.ts b/server/src/services/trash.service.spec.ts similarity index 69% rename from server/src/domain/trash/trash.service.spec.ts rename to server/src/services/trash.service.spec.ts index 81f4186e8..ecdf577ed 100644 --- a/server/src/domain/trash/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,23 +1,21 @@ import { BadRequestException } from '@nestjs/common'; -import { - IAccessRepositoryMock, - assetStub, - authStub, - newAccessRepositoryMock, - newAssetRepositoryMock, - newCommunicationRepositoryMock, - newJobRepositoryMock, -} from '@test'; -import { JobName } from '..'; -import { ClientEvent, IAssetRepository, ICommunicationRepository, IJobRepository } from '../repositories'; -import { TrashService } from './trash.service'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { TrashService } from 'src/services/trash.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; describe(TrashService.name, () => { let sut: TrashService; let accessMock: IAccessRepositoryMock; let assetMock: jest.Mocked; let jobMock: jest.Mocked; - let communicationMock: jest.Mocked; + let eventMock: jest.Mocked; it('should work', () => { expect(sut).toBeDefined(); @@ -26,10 +24,10 @@ describe(TrashService.name, () => { beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); - communicationMock = newCommunicationRepositoryMock(); + eventMock = newEventRepositoryMock(); jobMock = newJobRepositoryMock(); - sut = new TrashService(accessMock, assetMock, jobMock, communicationMock); + sut = new TrashService(accessMock, assetMock, jobMock, eventMock); }); describe('restoreAssets', () => { @@ -56,14 +54,14 @@ describe(TrashService.name, () => { assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); expect(assetMock.restoreAll).not.toHaveBeenCalled(); - expect(communicationMock.send).not.toHaveBeenCalled(); + expect(eventMock.clientSend).not.toHaveBeenCalled(); }); it('should restore and notify', async () => { assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]); - expect(communicationMock.send).toHaveBeenCalledWith(ClientEvent.ASSET_RESTORE, authStub.user1.user.id, [ + expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_RESTORE, authStub.user1.user.id, [ assetStub.image.id, ]); }); diff --git a/server/src/domain/trash/trash.service.ts b/server/src/services/trash.service.ts similarity index 70% rename from server/src/domain/trash/trash.service.ts rename to server/src/services/trash.service.ts index 30fd6843e..f74ea8098 100644 --- a/server/src/domain/trash/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,17 +1,13 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore, Permission } from '../access'; -import { BulkIdsDto } from '../asset'; -import { AuthDto } from '../auth'; -import { usePagination } from '../domain.util'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; -import { - ClientEvent, - IAccessRepository, - IAssetRepository, - ICommunicationRepository, - IJobRepository, -} from '../repositories'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { usePagination } from 'src/utils/pagination'; export class TrashService { private access: AccessCore; @@ -20,7 +16,7 @@ export class TrashService { @Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, ) { this.access = AccessCore.create(accessRepository); } @@ -64,6 +60,6 @@ export class TrashService { } await this.assetRepository.restoreAll(ids); - this.communicationRepository.send(ClientEvent.ASSET_RESTORE, auth.user.id, ids); + this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, auth.user.id, ids); } } diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/services/user.service.spec.ts similarity index 93% rename from server/src/domain/user/user.service.spec.ts rename to server/src/services/user.service.spec.ts index d0e56e4cd..973f644d3 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -1,37 +1,31 @@ -import { UserEntity, UserStatus } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { - authStub, - newAlbumRepositoryMock, - newCryptoRepositoryMock, - newJobRepositoryMock, - newLibraryRepositoryMock, - newStorageRepositoryMock, - newSystemConfigRepositoryMock, - newUserRepositoryMock, - systemConfigStub, - userStub, -} from '@test'; import { when } from 'jest-when'; -import { CacheControl, ImmichFileResponse } from '../domain.util'; -import { JobName } from '../job'; -import { - IAlbumRepository, - ICryptoRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, -} from '../repositories'; -import { UpdateUserDto } from './dto/update-user.dto'; -import { mapUser } from './response-dto'; -import { UserService } from './user.service'; +import { UpdateUserDto, mapUser } from 'src/dtos/user.dto'; +import { UserEntity, UserStatus } from 'src/entities/user.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { UserService } from 'src/services/user.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { authStub } from 'test/fixtures/auth.stub'; +import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; const makeDeletedAt = (daysAgo: number) => { const deletedAt = new Date(); diff --git a/server/src/domain/user/user.service.ts b/server/src/services/user.service.ts similarity index 84% rename from server/src/domain/user/user.service.ts rename to server/src/services/user.service.ts index a1db1fb04..a2bc8c7cf 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/services/user.service.ts @@ -1,27 +1,21 @@ -import { UserEntity, UserStatus } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { randomBytes } from 'node:crypto'; -import { AuthDto } from '../auth'; -import { CacheControl, ImmichFileResponse } from '../domain.util'; -import { IEntityJob, JobName } from '../job'; -import { - IAlbumRepository, - ICryptoRepository, - IJobRepository, - ILibraryRepository, - IStorageRepository, - ISystemConfigRepository, - IUserRepository, - JobStatus, - UserFindOptions, -} from '../repositories'; -import { StorageCore, StorageFolder } from '../storage'; -import { SystemConfigCore } from '../system-config/system-config.core'; -import { CreateUserDto, DeleteUserDto, UpdateUserDto } from './dto'; -import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto'; -import { UserCore } from './user.core'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { UserCore } from 'src/cores/user.core'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; +import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { UserEntity, UserStatus } from 'src/entities/user.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class UserService { @@ -31,7 +25,7 @@ export class UserService { constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, + @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) libraryRepository: ILibraryRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @@ -137,7 +131,7 @@ export class UserService { } const providedPassword = await ask(mapUser(admin)); - const password = providedPassword || randomBytes(24).toString('base64').replaceAll(/\W/g, ''); + const password = providedPassword || this.cryptoRepository.newPassword(24); await this.userCore.updateUser(admin, admin.id, { password }); diff --git a/server/src/infra/subscribers/audit.subscriber.ts b/server/src/subscribers/audit.subscriber.ts similarity index 86% rename from server/src/infra/subscribers/audit.subscriber.ts rename to server/src/subscribers/audit.subscriber.ts index 896f9ae5e..3d65507ae 100644 --- a/server/src/infra/subscribers/audit.subscriber.ts +++ b/server/src/subscribers/audit.subscriber.ts @@ -1,5 +1,7 @@ +import { AlbumEntity } from 'src/entities/album.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm'; -import { AlbumEntity, AssetEntity, AuditEntity, DatabaseAction, EntityType } from '../entities'; @EventSubscriber() export class AuditSubscriber implements EntitySubscriberInterface { diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts new file mode 100644 index 000000000..253073919 --- /dev/null +++ b/server/src/utils/asset.util.ts @@ -0,0 +1,91 @@ +import { AccessCore, Permission } from 'src/cores/access.core'; +import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setUnion } from 'src/utils/set'; + +export interface IBulkAsset { + getAssetIds: (id: string, assetIds: string[]) => Promise>; + addAssetIds: (id: string, assetIds: string[]) => Promise; + removeAssetIds: (id: string, assetIds: string[]) => Promise; +} + +export const addAssets = async ( + auth: AuthDto, + repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + dto: { id: string; assetIds: string[] }, +) => { + const { accessRepository, repository } = repositories; + const access = AccessCore.create(accessRepository); + + const existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds); + const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); + const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + + const results: BulkIdResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = existingAssetIds.has(assetId); + if (hasAsset) { + results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE }); + continue; + } + + const hasAccess = allowedAssetIds.has(assetId); + if (!hasAccess) { + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); + continue; + } + + results.push({ id: assetId, success: true }); + } + + const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); + if (newAssetIds.length > 0) { + await repository.addAssetIds(dto.id, newAssetIds); + } + + return results; +}; + +export const removeAssets = async ( + auth: AuthDto, + repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + dto: { id: string; assetIds: string[]; permissions: Permission[] }, +) => { + const { accessRepository, repository } = repositories; + const access = AccessCore.create(accessRepository); + + const existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds); + let allowedAssetIds = new Set(); + let remainingAssetIds = existingAssetIds; + + for (const permission of dto.permissions) { + const newAssetIds = await access.checkAccess(auth, permission, setDifference(remainingAssetIds, allowedAssetIds)); + remainingAssetIds = setDifference(remainingAssetIds, newAssetIds); + allowedAssetIds = setUnion(allowedAssetIds, newAssetIds); + } + + const results: BulkIdResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = existingAssetIds.has(assetId); + if (!hasAsset) { + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND }); + continue; + } + + const hasAccess = allowedAssetIds.has(assetId); + if (!hasAccess) { + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); + continue; + } + + results.push({ id: assetId, success: true }); + } + + const removedIds = results.filter(({ success }) => success).map(({ id }) => id); + if (removedIds.length > 0) { + await repository.removeAssetIds(dto.id, removedIds); + } + + return results; +}; diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts new file mode 100644 index 000000000..e837c81b9 --- /dev/null +++ b/server/src/utils/bytes.ts @@ -0,0 +1,24 @@ +const KiB = Math.pow(1024, 1); +const MiB = Math.pow(1024, 2); +const GiB = Math.pow(1024, 3); +const TiB = Math.pow(1024, 4); +const PiB = Math.pow(1024, 5); + +export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; + +export function asHumanReadable(bytes: number, precision = 1): string { + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + + let magnitude = 0; + let remainder = bytes; + while (remainder >= 1024) { + if (magnitude + 1 < units.length) { + magnitude++; + remainder /= 1024; + } else { + break; + } + } + + return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; +} diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts new file mode 100644 index 000000000..be0eb8fa6 --- /dev/null +++ b/server/src/utils/database.ts @@ -0,0 +1,139 @@ +import _ from 'lodash'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface'; +import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } 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 asVector = (embedding: number[], quote = false) => + quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; + +export function searchAssetBuilder( + builder: SelectQueryBuilder, + options: AssetSearchBuilderOptions, +): SelectQueryBuilder { + builder.andWhere( + _.omitBy( + { + createdAt: OptionalBetween(options.createdAfter, options.createdBefore), + updatedAt: OptionalBetween(options.updatedAfter, options.updatedBefore), + deletedAt: OptionalBetween(options.trashedAfter, options.trashedBefore), + fileCreatedAt: OptionalBetween(options.takenAfter, options.takenBefore), + }, + _.isUndefined, + ), + ); + + const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined); + const hasExifQuery = Object.keys(exifInfo).length > 0; + + if (options.withExif && !hasExifQuery) { + builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo'); + } + + if (hasExifQuery) { + options.withExif + ? builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo') + : builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); + + builder.andWhere({ exifInfo }); + } + + const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']); + builder.andWhere(_.omitBy(id, _.isUndefined)); + + if (options.userIds) { + builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); + } + + const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); + builder.andWhere(_.omitBy(path, _.isUndefined)); + + if (options.originalFileName) { + builder.andWhere(`f_unaccent(${builder.alias}.originalFileName) ILIKE f_unaccent(:originalFileName)`, { + originalFileName: `%${options.originalFileName}%`, + }); + } + + const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']); + const { + isArchived, + isEncoded, + isMotion, + withArchived, + isNotInAlbum, + withFaces, + withPeople, + withSmartInfo, + personIds, + withExif, + withStacked, + trashedAfter, + trashedBefore, + } = options; + builder.andWhere( + _.omitBy( + { + ...status, + isArchived: isArchived ?? (withArchived ? undefined : false), + encodedVideoPath: isEncoded ? Not(IsNull()) : undefined, + livePhotoVideoId: isMotion ? Not(IsNull()) : undefined, + }, + _.isUndefined, + ), + ); + + if (isNotInAlbum) { + builder + .leftJoin(`${builder.alias}.albums`, 'albums') + .andWhere('albums.id IS NULL') + .andWhere(`${builder.alias}.isVisible = true`); + } + + if (withFaces || withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.faces`, 'faces'); + } + + if (withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.person`, 'person'); + } + + if (withSmartInfo) { + builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo'); + } + + if (personIds && personIds.length > 0) { + builder + .leftJoin(`${builder.alias}.faces`, 'faces') + .andWhere('faces.personId IN (:...personIds)', { personIds }) + .addGroupBy(`${builder.alias}.id`) + .having('COUNT(DISTINCT faces.personId) = :personCount', { personCount: personIds.length }); + + if (withExif) { + builder.addGroupBy('exifInfo.assetId'); + } + } + + if (withStacked) { + builder.leftJoinAndSelect(`${builder.alias}.stack`, 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets'); + } + + const withDeleted = options.withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); + if (withDeleted) { + builder.withDeleted(); + } + + return builder; +} diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts new file mode 100644 index 000000000..a80f17bea --- /dev/null +++ b/server/src/utils/file.ts @@ -0,0 +1,88 @@ +import { HttpException, StreamableFile } from '@nestjs/common'; +import { NextFunction, Response } from 'express'; +import { access, constants } from 'node:fs/promises'; +import { basename, extname, isAbsolute } from 'node:path'; +import { promisify } from 'node:util'; +import { ImmichReadStream } from 'src/interfaces/storage.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { isConnectionAborted } from 'src/utils/misc'; + +export function getFileNameWithoutExtension(path: string): string { + return basename(path, extname(path)); +} + +export function getLivePhotoMotionFilename(stillName: string, motionName: string) { + return getFileNameWithoutExtension(stillName) + extname(motionName); +} + +export enum CacheControl { + PRIVATE_WITH_CACHE = 'private_with_cache', + PRIVATE_WITHOUT_CACHE = 'private_without_cache', + NONE = 'none', +} + +export class ImmichFileResponse { + public readonly path!: string; + public readonly contentType!: string; + public readonly cacheControl!: CacheControl; + + constructor(response: ImmichFileResponse) { + Object.assign(this, response); + } +} +type SendFile = Parameters; +type SendFileOptions = SendFile[1]; + +const logger = new ImmichLogger('SendFile'); + +export const sendFile = async ( + res: Response, + next: NextFunction, + handler: () => Promise, +): Promise => { + const _sendFile = (path: string, options: SendFileOptions) => + promisify(res.sendFile).bind(res)(path, options); + + try { + const file = await handler(); + switch (file.cacheControl) { + case CacheControl.PRIVATE_WITH_CACHE: { + res.set('Cache-Control', 'private, max-age=86400, no-transform'); + break; + } + + case CacheControl.PRIVATE_WITHOUT_CACHE: { + res.set('Cache-Control', 'private, no-cache, no-transform'); + break; + } + } + + res.header('Content-Type', file.contentType); + + const options: SendFileOptions = { dotfiles: 'allow' }; + if (!isAbsolute(file.path)) { + options.root = process.cwd(); + } + + await access(file.path, constants.R_OK); + + return _sendFile(file.path, options); + } catch (error: Error | any) { + // ignore client-closed connection + if (isConnectionAborted(error)) { + return; + } + + // log non-http errors + if (error instanceof HttpException === false) { + logger.error(`Unable to send file: ${error.name}`, error.stack); + } + + res.header('Cache-Control', 'none'); + next(error); + } +}; + +export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => { + return new StreamableFile(stream, { type, length }); +}; diff --git a/server/src/infra/instrumentation.ts b/server/src/utils/instrumentation.ts similarity index 86% rename from server/src/infra/instrumentation.ts rename to server/src/utils/instrumentation.ts index 130eaea3d..872ff2de6 100644 --- a/server/src/infra/instrumentation.ts +++ b/server/src/utils/instrumentation.ts @@ -1,4 +1,3 @@ -import { serverVersion } from '@app/domain/domain.constant'; import { Histogram, MetricOptions, ValueType, metrics } from '@opentelemetry/api'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; @@ -14,16 +13,20 @@ import { snakeCase, startCase } from 'lodash'; import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; import { performance } from 'node:perf_hooks'; -import { excludePaths } from './infra.config'; -import { DecorateAll } from './infra.utils'; +import { excludePaths, serverVersion } from 'src/constants'; +import { DecorateAll } from 'src/decorators'; let metricsEnabled = process.env.IMMICH_METRICS === 'true'; -const hostMetrics = +export const hostMetrics = process.env.IMMICH_HOST_METRICS == null ? metricsEnabled : process.env.IMMICH_HOST_METRICS === 'true'; -const apiMetrics = process.env.IMMICH_API_METRICS == null ? metricsEnabled : process.env.IMMICH_API_METRICS === 'true'; -const repoMetrics = process.env.IMMICH_IO_METRICS == null ? metricsEnabled : process.env.IMMICH_IO_METRICS === 'true'; +export const apiMetrics = + process.env.IMMICH_API_METRICS == null ? metricsEnabled : process.env.IMMICH_API_METRICS === 'true'; +export const repoMetrics = + process.env.IMMICH_IO_METRICS == null ? metricsEnabled : process.env.IMMICH_IO_METRICS === 'true'; +export const jobMetrics = + process.env.IMMICH_JOB_METRICS == null ? metricsEnabled : process.env.IMMICH_JOB_METRICS === 'true'; -metricsEnabled ||= hostMetrics || apiMetrics || repoMetrics; +metricsEnabled ||= hostMetrics || apiMetrics || repoMetrics || jobMetrics; if (!metricsEnabled && process.env.OTEL_SDK_DISABLED === undefined) { process.env.OTEL_SDK_DISABLED = 'true'; } diff --git a/server/src/infra/logger.ts b/server/src/utils/logger.ts similarity index 92% rename from server/src/infra/logger.ts rename to server/src/utils/logger.ts index 8de149c40..fef13a8fb 100644 --- a/server/src/infra/logger.ts +++ b/server/src/utils/logger.ts @@ -1,6 +1,6 @@ import { ConsoleLogger } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; -import { LogLevel } from './entities'; +import { LogLevel } from 'src/entities/system-config.entity'; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; diff --git a/server/src/domain/media/media.util.ts b/server/src/utils/media.ts similarity index 99% rename from server/src/domain/media/media.util.ts rename to server/src/utils/media.ts index 3acabb435..5f1218766 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/utils/media.ts @@ -1,4 +1,5 @@ -import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from '@app/infra/entities'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; import { AudioStreamInfo, BitrateDistribution, @@ -6,8 +7,8 @@ import { VideoCodecHWConfig, VideoCodecSWConfig, VideoStreamInfo, -} from '../repositories'; -import { SystemConfigFFmpegDto } from '../system-config/dto'; +} from 'src/interfaces/media.interface'; + class BaseConfig implements VideoCodecSWConfig { presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; constructor(protected config: SystemConfigFFmpegDto) {} diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/utils/mime-types.spec.ts similarity index 68% rename from server/src/domain/domain.constant.spec.ts rename to server/src/utils/mime-types.spec.ts index 70944328e..bce75e1e1 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -1,4 +1,4 @@ -import { Version, VersionType, mimeTypes } from './domain.constant'; +import { mimeTypes } from 'src/utils/mime-types'; describe('mimeTypes', () => { for (const { mimetype, extension } of [ @@ -75,10 +75,13 @@ describe('mimeTypes', () => { { mimetype: 'image/x-sony-srf', extension: '.srf' }, { mimetype: 'image/x3f', extension: '.x3f' }, { mimetype: 'video/3gpp', extension: '.3gp' }, + { mimetype: 'video/3gpp', extension: '.3gpp' }, { mimetype: 'video/avi', extension: '.avi' }, { mimetype: 'video/mp2t', extension: '.m2ts' }, { mimetype: 'video/mp2t', extension: '.mts' }, { mimetype: 'video/mp4', extension: '.mp4' }, + { mimetype: 'video/mpeg', extension: '.mpe' }, + { mimetype: 'video/mpeg', extension: '.mpeg' }, { mimetype: 'video/mpeg', extension: '.mpg' }, { mimetype: 'video/msvideo', extension: '.avi' }, { mimetype: 'video/quicktime', extension: '.mov' }, @@ -196,74 +199,3 @@ describe('mimeTypes', () => { } }); }); - -describe('Version', () => { - const tests = [ - { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH }, - { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR }, - { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH }, - { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR }, - { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL }, - { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, - { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR }, - { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, - ]; - - describe('isOlderThan', () => { - for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { - const expected = compare < 0 ? type : VersionType.EQUAL; - it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected); - }); - } - }); - - describe('isEqual', () => { - for (const { this: thisVersion, other: otherVersion, compare } of tests) { - const bool = compare === 0; - it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isEqual(otherVersion)).toEqual(bool); - }); - } - }); - - describe('isNewerThan', () => { - for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { - const expected = compare > 0 ? type : VersionType.EQUAL; - it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected); - }); - } - }); - - describe('fromString', () => { - const tests = [ - { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) }, - { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) }, - { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) }, - { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) }, - { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) }, - { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) }, - { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) }, - ]; - - for (const { scenario, value, expected } of tests) { - it(`should correctly parse ${scenario}`, () => { - const actual = Version.fromString(value); - expect(actual.major).toEqual(expected.major); - expect(actual.minor).toEqual(expected.minor); - expect(actual.patch).toEqual(expected.patch); - }); - } - }); -}); diff --git a/server/src/domain/domain.constant.ts b/server/src/utils/mime-types.ts similarity index 55% rename from server/src/domain/domain.constant.ts rename to server/src/utils/mime-types.ts index 56b455855..a888e4f42 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/utils/mime-types.ts @@ -1,96 +1,5 @@ -import { AssetType } from '@app/infra/entities'; -import { Duration } from 'luxon'; -import { readFileSync } from 'node:fs'; -import { extname, join } from 'node:path'; - -export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); -export const ONE_HOUR = Duration.fromObject({ hours: 1 }); - -export interface IVersion { - major: number; - minor: number; - patch: number; -} - -export enum VersionType { - EQUAL = 0, - PATCH = 1, - MINOR = 2, - MAJOR = 3, -} - -export class Version implements IVersion { - public readonly types = ['major', 'minor', 'patch'] as const; - - constructor( - public major: number, - public minor: number = 0, - public patch: number = 0, - ) {} - - toString() { - return `${this.major}.${this.minor}.${this.patch}`; - } - - toJSON() { - const { major, minor, patch } = this; - return { major, minor, patch }; - } - - static fromString(version: string): Version { - const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i; - const matchResult = version.match(regex); - if (matchResult) { - const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string }; - return new Version(Number(major), Number(minor), Number(patch)); - } else { - throw new Error(`Invalid version format: ${version}`); - } - } - - private compare(version: Version): [number, VersionType] { - for (const [i, key] of this.types.entries()) { - const diff = this[key] - version[key]; - if (diff !== 0) { - return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType]; - } - } - - return [0, VersionType.EQUAL]; - } - - isOlderThan(version: Version): VersionType { - const [bool, type] = this.compare(version); - return bool < 0 ? type : VersionType.EQUAL; - } - - isEqual(version: Version): boolean { - const [bool] = this.compare(version); - return bool === 0; - } - - isNewerThan(version: Version): VersionType { - const [bool, type] = this.compare(version); - return bool > 0 ? type : VersionType.EQUAL; - } -} - -export const envName = (process.env.NODE_ENV || 'development').toUpperCase(); -export const isDev = process.env.NODE_ENV === 'development'; - -const { version } = JSON.parse(readFileSync('./package.json', 'utf8')); -export const serverVersion = Version.fromString(version); - -export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; -export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www'; - -const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources'; - -export const citiesFile = 'cities500.txt'; -export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt'); -export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt'); -export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt'); -export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); +import { extname } from 'node:path'; +import { AssetType } from 'src/entities/asset.entity'; const image: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], @@ -147,6 +56,7 @@ const profile: Record = Object.fromEntries( const video: Record = { '.3gp': ['video/3gpp'], + '.3gpp': ['video/3gpp'], '.avi': ['video/avi', 'video/msvideo', 'video/vnd.avi', 'video/x-msvideo'], '.flv': ['video/x-flv'], '.insv': ['video/mp4'], @@ -155,6 +65,8 @@ const video: Record = { '.mkv': ['video/x-matroska'], '.mov': ['video/quicktime'], '.mp4': ['video/mp4'], + '.mpe': ['video/mpeg'], + '.mpeg': ['video/mpeg'], '.mpg': ['video/mpeg'], '.mts': ['video/mp2t'], '.webm': ['video/webm'], diff --git a/server/src/immich/app.utils.ts b/server/src/utils/misc.ts similarity index 65% rename from server/src/immich/app.utils.ts rename to server/src/utils/misc.ts index 26aca2a9b..3837c6279 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/utils/misc.ts @@ -1,15 +1,4 @@ -import { - CacheControl, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_API_KEY_NAME, - ImmichFileResponse, - ImmichReadStream, - isConnectionAborted, - serverVersion, -} from '@app/domain'; -import { ImmichLogger } from '@app/infra/logger'; -import { HttpException, INestApplication, StreamableFile } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { DocumentBuilder, OpenAPIObject, @@ -18,70 +7,48 @@ import { SwaggerModule, } from '@nestjs/swagger'; import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { NextFunction, Response } from 'express'; import _ from 'lodash'; import { writeFileSync } from 'node:fs'; -import { access, constants } from 'node:fs/promises'; -import path, { isAbsolute } from 'node:path'; -import { promisify } from 'node:util'; -import { Metadata } from './app.guard'; +import path from 'node:path'; +import { + CLIP_MODEL_INFO, + IMMICH_ACCESS_COOKIE, + IMMICH_API_KEY_HEADER, + IMMICH_API_KEY_NAME, + serverVersion, +} from 'src/constants'; +import { Metadata } from 'src/middleware/auth.guard'; +import { ImmichLogger } from 'src/utils/logger'; -type SendFile = Parameters; -type SendFileOptions = SendFile[1]; +export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; -const logger = new ImmichLogger('SendFile'); +export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => { + promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); +}; -export const sendFile = async ( - res: Response, - next: NextFunction, - handler: () => Promise, -): Promise => { - const _sendFile = (path: string, options: SendFileOptions) => - promisify(res.sendFile).bind(res)(path, options); +export interface OpenGraphTags { + title: string; + description: string; + imageUrl?: string; +} - try { - const file = await handler(); - switch (file.cacheControl) { - case CacheControl.PRIVATE_WITH_CACHE: { - res.set('Cache-Control', 'private, max-age=86400, no-transform'); - break; - } - - case CacheControl.PRIVATE_WITHOUT_CACHE: { - res.set('Cache-Control', 'private, no-cache, no-transform'); - break; - } - } - - res.header('Content-Type', file.contentType); - - const options: SendFileOptions = { dotfiles: 'allow' }; - if (!isAbsolute(file.path)) { - options.root = process.cwd(); - } - - await access(file.path, constants.R_OK); - - return _sendFile(file.path, options); - } catch (error: Error | any) { - // ignore client-closed connection - if (isConnectionAborted(error)) { - return; - } - - // log non-http errors - if (error instanceof HttpException === false) { - logger.error(`Unable to send file: ${error.name}`, error.stack); - } - - res.header('Cache-Control', 'none'); - next(error); +function cleanModelName(modelName: string): string { + const token = modelName.split('/').at(-1); + if (!token) { + throw new Error(`Invalid model name: ${modelName}`); } -}; -export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => { - return new StreamableFile(stream, { type, length }); -}; + return token.replaceAll(':', '_'); +} + +export function getCLIPModelInfo(modelName: string) { + const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)]; + if (!modelInfo) { + throw new Error(`Unknown CLIP model: ${modelName}`); + } + + return modelInfo; +} function sortKeys(target: T): T { if (!target || typeof target !== 'object' || Array.isArray(target)) { diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts new file mode 100644 index 000000000..dec1a9de0 --- /dev/null +++ b/server/src/utils/pagination.ts @@ -0,0 +1,79 @@ +import _ from 'lodash'; +import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; + +export interface PaginationOptions { + take: number; + skip?: number; +} + +export enum PaginationMode { + LIMIT_OFFSET = 'limit-offset', + SKIP_TAKE = 'skip-take', +} + +export interface PaginatedBuilderOptions { + take: number; + skip?: number; + mode?: PaginationMode; +} + +export interface PaginationResult { + items: T[]; + hasNextPage: boolean; +} + +export type Paginated = Promise>; + +export async function* usePagination( + pageSize: number, + getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, +) { + let hasNextPage = true; + + for (let skip = 0; hasNextPage; skip += pageSize) { + const result = await getNextPage({ take: pageSize, skip }); + hasNextPage = result.hasNextPage; + yield result.items; + } +} + +function paginationHelper(items: Entity[], take: number): PaginationResult { + const hasNextPage = items.length > take; + items.splice(take); + + return { items, hasNextPage }; +} + +export async function paginate( + repository: Repository, + { take, skip }: PaginationOptions, + searchOptions?: FindManyOptions, +): Paginated { + const items = await repository.find( + _.omitBy( + { + ...searchOptions, + // Take one more item to check if there's a next page + take: take + 1, + skip, + }, + _.isUndefined, + ), + ); + + return paginationHelper(items, take); +} + +export async function paginatedBuilder( + qb: SelectQueryBuilder, + { take, skip, mode }: PaginatedBuilderOptions, +): Paginated { + if (mode === PaginationMode.LIMIT_OFFSET) { + qb.limit(take + 1).offset(skip); + } else { + qb.take(take + 1).skip(skip); + } + + const items = await qb.getMany(); + return paginationHelper(items, take); +} diff --git a/server/src/utils/set.ts b/server/src/utils/set.ts new file mode 100644 index 000000000..971d3f7e5 --- /dev/null +++ b/server/src/utils/set.ts @@ -0,0 +1,36 @@ +// NOTE: The following Set utils have been added here, to easily determine where they are used. +// They should be replaced with native Set operations, when they are added to the language. +// Proposal reference: https://github.com/tc39/proposal-set-methods + +export const setUnion = (...sets: Set[]): Set => { + const union = new Set(sets[0]); + for (const set of sets.slice(1)) { + for (const element of set) { + union.add(element); + } + } + return union; +}; + +export const setDifference = (setA: Set, ...sets: Set[]): Set => { + const difference = new Set(setA); + for (const set of sets) { + for (const element of set) { + difference.delete(element); + } + } + return difference; +}; + +export const setIsSuperset = (set: Set, subset: Set): boolean => { + for (const element of subset) { + if (!set.has(element)) { + return false; + } + } + return true; +}; + +export const setIsEqual = (setA: Set, setB: Set): boolean => { + return setA.size === setB.size && setIsSuperset(setA, setB); +}; diff --git a/server/src/infra/sql-generator/index.ts b/server/src/utils/sql.ts similarity index 69% rename from server/src/infra/sql-generator/index.ts rename to server/src/utils/sql.ts index 0b10c018c..662c40fcb 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/utils/sql.ts @@ -1,55 +1,50 @@ #!/usr/bin/env node -import { ISystemConfigRepository } from '@app/domain'; import { INestApplication } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { OpenTelemetryModule } from 'nestjs-otel'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { databaseConfig } from '../database.config'; -import { databaseEntities } from '../entities'; -import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util'; -import { - AccessRepository, - AlbumRepository, - ApiKeyRepository, - AssetRepository, - AuditRepository, - LibraryRepository, - MoveRepository, - PartnerRepository, - PersonRepository, - SearchRepository, - SharedLinkRepository, - SystemConfigRepository, - SystemMetadataRepository, - TagRepository, - UserRepository, - UserTokenRepository, -} from '../repositories'; -import { SqlLogger } from './sql.logger'; +import { format } from 'sql-formatter'; +import { databaseConfig } from 'src/database.config'; +import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators'; +import { entities } from 'src/entities'; +import { repositories } from 'src/repositories'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { AuthService } from 'src/services/auth.service'; +import { otelConfig } from 'src/utils/instrumentation'; +import { Logger } from 'typeorm'; + +export class SqlLogger implements Logger { + queries: string[] = []; + errors: Array<{ error: string | Error; query: string }> = []; + + clear() { + this.queries = []; + this.errors = []; + } + + logQuery(query: string) { + this.queries.push(format(query, { language: 'postgresql' })); + } + + logQueryError(error: string | Error, query: string) { + this.errors.push({ error, query }); + } + + logQuerySlow() {} + logSchemaBuild() {} + logMigration() {} + log() {} +} const reflector = new Reflector(); -const repositories = [ - AccessRepository, - AlbumRepository, - ApiKeyRepository, - AssetRepository, - AuditRepository, - LibraryRepository, - MoveRepository, - PartnerRepository, - PersonRepository, - SharedLinkRepository, - SearchRepository, - SystemConfigRepository, - SystemMetadataRepository, - TagRepository, - UserTokenRepository, - UserRepository, -]; -type Repository = (typeof repositories)[0]; +type Repository = (typeof repositories)[0]['useClass']; +type Provider = { provide: any; useClass: Repository }; type SqlGeneratorOptions = { targetDir: string }; class SqlGenerator { @@ -62,8 +57,8 @@ class SqlGenerator { async run() { try { await this.setup(); - for (const Repository of repositories) { - await this.process(Repository); + for (const repository of repositories) { + await this.process(repository); } await this.write(); this.stats(); @@ -80,25 +75,27 @@ class SqlGenerator { imports: [ TypeOrmModule.forRoot({ ...databaseConfig, - entities: databaseEntities, + entities, logging: ['query'], logger: this.sqlLogger, }), - TypeOrmModule.forFeature(databaseEntities), + TypeOrmModule.forFeature(entities), + EventEmitterModule.forRoot(), + OpenTelemetryModule.forRoot(otelConfig), ], - providers: [{ provide: ISystemConfigRepository, useClass: SystemConfigRepository }, ...repositories], + providers: [...repositories, AuthService, SchedulerRegistry], }).compile(); this.app = await moduleFixture.createNestApplication().init(); } - async process(Repository: Repository) { + async process({ provide: token, useClass: Repository }: Provider) { if (!this.app) { throw new Error('Not initialized'); } const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`]; - const instance = this.app.get(Repository); + const instance = this.app.get(token); // normal repositories data.push(...(await this.runTargets(instance, `${Repository.name}`))); @@ -158,6 +155,10 @@ class SqlGenerator { private async write() { for (const [repoName, data] of Object.entries(this.results)) { + // only contains the header + if (data.length === 1) { + continue; + } const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', ''); const file = join(this.options.targetDir, `${filename}.sql`); await writeFile(file, data.join('\n\n') + '\n'); @@ -180,7 +181,7 @@ class SqlGenerator { } } -new SqlGenerator({ targetDir: './src/infra/sql' }) +new SqlGenerator({ targetDir: './src/queries' }) .run() .then(() => { console.log('Done'); diff --git a/server/src/utils/version.spec.ts b/server/src/utils/version.spec.ts new file mode 100644 index 000000000..34c8abb41 --- /dev/null +++ b/server/src/utils/version.spec.ts @@ -0,0 +1,72 @@ +import { Version, VersionType } from 'src/utils/version'; + +describe('Version', () => { + const tests = [ + { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH }, + { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR }, + { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR }, + { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR }, + { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, + ]; + + describe('isOlderThan', () => { + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare < 0 ? type : VersionType.EQUAL; + it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected); + }); + } + }); + + describe('isEqual', () => { + for (const { this: thisVersion, other: otherVersion, compare } of tests) { + const bool = compare === 0; + it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isEqual(otherVersion)).toEqual(bool); + }); + } + }); + + describe('isNewerThan', () => { + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare > 0 ? type : VersionType.EQUAL; + it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected); + }); + } + }); + + describe('fromString', () => { + const tests = [ + { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) }, + { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) }, + { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) }, + { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) }, + { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) }, + { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) }, + { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) }, + ]; + + for (const { scenario, value, expected } of tests) { + it(`should correctly parse ${scenario}`, () => { + const actual = Version.fromString(value); + expect(actual.major).toEqual(expected.major); + expect(actual.minor).toEqual(expected.minor); + expect(actual.patch).toEqual(expected.patch); + }); + } + }); +}); diff --git a/server/src/utils/version.ts b/server/src/utils/version.ts new file mode 100644 index 000000000..6eca12eb4 --- /dev/null +++ b/server/src/utils/version.ts @@ -0,0 +1,64 @@ +export type IVersion = { major: number; minor: number; patch: number }; + +export enum VersionType { + EQUAL = 0, + PATCH = 1, + MINOR = 2, + MAJOR = 3, +} + +export class Version implements IVersion { + public readonly types = ['major', 'minor', 'patch'] as const; + + constructor( + public major: number, + public minor: number = 0, + public patch: number = 0, + ) {} + + toString() { + return `${this.major}.${this.minor}.${this.patch}`; + } + + toJSON() { + const { major, minor, patch } = this; + return { major, minor, patch }; + } + + static fromString(version: string): Version { + const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i; + const matchResult = version.match(regex); + if (matchResult) { + const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string }; + return new Version(Number(major), Number(minor), Number(patch)); + } else { + throw new Error(`Invalid version format: ${version}`); + } + } + + private compare(version: Version): [number, VersionType] { + for (const [i, key] of this.types.entries()) { + const diff = this[key] - version[key]; + if (diff !== 0) { + return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType]; + } + } + + return [0, VersionType.EQUAL]; + } + + isOlderThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool < 0 ? type : VersionType.EQUAL; + } + + isEqual(version: Version): boolean { + const [bool] = this.compare(version); + return bool === 0; + } + + isNewerThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool > 0 ? type : VersionType.EQUAL; + } +} diff --git a/server/src/validation.ts b/server/src/validation.ts new file mode 100644 index 000000000..bc1dbae81 --- /dev/null +++ b/server/src/validation.ts @@ -0,0 +1,164 @@ +import { + ArgumentMetadata, + BadRequestException, + FileValidator, + Injectable, + ParseUUIDPipe, + applyDecorators, +} from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsDate, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateIf, + ValidationOptions, + isDateString, +} from 'class-validator'; +import { CronJob } from 'cron'; +import sanitize from 'sanitize-filename'; + +@Injectable() +export class ParseMeUUIDPipe extends ParseUUIDPipe { + async transform(value: string, metadata: ArgumentMetadata) { + if (value == 'me') { + return value; + } + return super.transform(value, metadata); + } +} + +@Injectable() +export class FileNotEmptyValidator extends FileValidator { + constructor(private requiredFields: string[]) { + super({}); + this.requiredFields = requiredFields; + } + + isValid(files?: any): boolean { + if (!files) { + return false; + } + + return this.requiredFields.every((field) => files[field]); + } + + buildErrorMessage(): string { + return `Field(s) ${this.requiredFields.join(', ')} should not be empty`; + } +} + +export class UUIDParamDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + id!: string; +} + +export interface OptionalOptions extends ValidationOptions { + nullable?: boolean; +} + +/** + * Checks if value is missing and if so, ignores all validators. + * + * @param validationOptions {@link OptionalOptions} + * + * @see IsOptional exported from `class-validator. + */ +// https://stackoverflow.com/a/71353929 +export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) { + if (nullable === true) { + return IsOptional(validationOptions); + } + + return ValidateIf((object: any, v: any) => v !== undefined, validationOptions); +} + +type UUIDOptions = { optional?: boolean; each?: boolean }; +export const ValidateUUID = (options?: UUIDOptions) => { + const { optional, each } = { optional: false, each: false, ...options }; + return applyDecorators( + IsUUID('4', { each }), + ApiProperty({ format: 'uuid' }), + optional ? Optional() : IsNotEmpty(), + each ? IsArray() : IsString(), + ); +}; + +type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' }; +export const ValidateDate = (options?: DateOptions) => { + const { optional, nullable, format } = { optional: false, nullable: false, format: 'date-time', ...options }; + + const decorators = [ + ApiProperty({ format }), + IsDate(), + optional ? Optional({ nullable: true }) : IsNotEmpty(), + Transform(({ key, value }) => { + if (value === null || value === undefined) { + return value; + } + + if (!isDateString(value)) { + throw new BadRequestException(`${key} must be a date string`); + } + + return new Date(value as string); + }), + ]; + + if (optional) { + decorators.push(Optional({ nullable })); + } + + return applyDecorators(...decorators); +}; + +type BooleanOptions = { optional?: boolean }; +export const ValidateBoolean = (options?: BooleanOptions) => { + const { optional } = { optional: false, ...options }; + const decorators = [ + // ApiProperty(), + IsBoolean(), + Transform(({ value }) => { + if (value == 'true') { + return true; + } else if (value == 'false') { + return false; + } + return value; + }), + ]; + + if (optional) { + decorators.push(Optional()); + } + + return applyDecorators(...decorators); +}; + +export function validateCronExpression(expression: string) { + try { + new CronJob(expression, () => {}); + } catch { + return false; + } + + return true; +} + +type IValue = { value: string }; + +export const toEmail = ({ value }: IValue) => value?.toLowerCase(); + +export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', '')); + +export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { + const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; + return Number.isInteger(value) && value >= min && value <= max; +}; diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts index 7a3aae5ff..4805f6604 100644 --- a/server/test/fixtures/activity.stub.ts +++ b/server/test/fixtures/activity.stub.ts @@ -1,8 +1,8 @@ -import { ActivityEntity } from '@app/infra/entities'; -import { albumStub } from './album.stub'; -import { assetStub } from './asset.stub'; -import { authStub } from './auth.stub'; -import { userStub } from './user.stub'; +import { ActivityEntity } from 'src/entities/activity.entity'; +import { albumStub } from 'test/fixtures/album.stub'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { userStub } from 'test/fixtures/user.stub'; export const activityStub = { oneComment: Object.freeze({ diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index bfb6acb6d..ff9348167 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,7 +1,7 @@ -import { AlbumEntity, AssetOrder } from '@app/infra/entities'; -import { assetStub } from './asset.stub'; -import { authStub } from './auth.stub'; -import { userStub } from './user.stub'; +import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { userStub } from 'test/fixtures/user.stub'; export const albumStub = { empty: Object.freeze({ diff --git a/server/test/fixtures/api-key.stub.ts b/server/test/fixtures/api-key.stub.ts index de1b8dc17..954c8f35a 100644 --- a/server/test/fixtures/api-key.stub.ts +++ b/server/test/fixtures/api-key.stub.ts @@ -1,6 +1,6 @@ -import { APIKeyEntity } from '@app/infra/entities'; -import { authStub } from './auth.stub'; -import { userStub } from './user.stub'; +import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { authStub } from 'test/fixtures/auth.stub'; +import { userStub } from 'test/fixtures/user.stub'; export const keyStub = { admin: Object.freeze({ diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index d72a295d4..0b2ff82a3 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,8 +1,10 @@ -import { AssetEntity, AssetStackEntity, AssetType, ExifEntity } from '@app/infra/entities'; -import { authStub } from './auth.stub'; -import { fileStub } from './file.stub'; -import { libraryStub } from './library.stub'; -import { userStub } from './user.stub'; +import { AssetStackEntity } from 'src/entities/asset-stack.entity'; +import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; +import { authStub } from 'test/fixtures/auth.stub'; +import { fileStub } from 'test/fixtures/file.stub'; +import { libraryStub } from 'test/fixtures/library.stub'; +import { userStub } from 'test/fixtures/user.stub'; export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetStackEntity => { return { @@ -24,10 +26,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - resizePath: null, + previewPath: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -60,10 +62,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -100,10 +102,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -137,10 +139,10 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/admin-id/thumbs/path.jpg', + previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/admin-id/webp/path.ext', + thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -182,10 +184,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -222,10 +224,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -262,10 +264,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -302,10 +304,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -342,10 +344,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -383,10 +385,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -454,10 +456,10 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -497,11 +499,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -533,11 +535,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -570,11 +572,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -608,10 +610,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -637,4 +639,82 @@ export const assetStub = { } as ExifEntity, deletedAt: null, }), + missingFileExtension: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/data/user1/photo.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + isExternal: true, + duration: null, + isVisible: true, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.externalLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'photo', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + } as ExifEntity, + }), + hasFileExtension: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/data/user1/photo.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + isExternal: true, + duration: null, + isVisible: true, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.externalLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'photo.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + } as ExifEntity, + }), }; diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts index ab1ca98b9..bca1d3349 100644 --- a/server/test/fixtures/audit.stub.ts +++ b/server/test/fixtures/audit.stub.ts @@ -1,5 +1,5 @@ -import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities'; -import { authStub } from './auth.stub'; +import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { authStub } from 'test/fixtures/auth.stub'; export const auditStub = { create: Object.freeze({ diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 79993a4da..2e56d0001 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,5 +1,7 @@ -import { AuthDto } from '@app/domain'; -import { SharedLinkEntity, UserEntity, UserTokenEntity } from '../../src/infra/entities'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { UserEntity } from 'src/entities/user.entity'; export const adminSignupStub = { name: 'Immich Admin', diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index 0b988d59a..2d2acec40 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -1,6 +1,6 @@ -import { AssetFaceEntity } from '@app/infra/entities'; -import { assetStub } from './asset.stub'; -import { personStub } from './person.stub'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { personStub } from 'test/fixtures/person.stub'; type NonNullableProperty = { [P in keyof T]: NonNullable }; diff --git a/server/test/fixtures/index.ts b/server/test/fixtures/index.ts deleted file mode 100644 index 2217c9b1f..000000000 --- a/server/test/fixtures/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from './album.stub'; -export * from './api-key.stub'; -export * from './asset.stub'; -export * from './audit.stub'; -export * from './auth.stub'; -export * from './error.stub'; -export * from './face.stub'; -export * from './file.stub'; -export * from './library.stub'; -export * from './media.stub'; -export * from './partner.stub'; -export * from './person.stub'; -export * from './shared-link.stub'; -export * from './system-config.stub'; -export * from './tag.stub'; -export * from './user-token.stub'; -export * from './user.stub'; -export * from './uuid.stub'; diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index db7687f28..dde250a7a 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -1,7 +1,8 @@ -import { APP_MEDIA_LOCATION, THUMBNAIL_DIR } from '@app/domain'; -import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { join } from 'node:path'; -import { userStub } from './user.stub'; +import { APP_MEDIA_LOCATION } from 'src/constants'; +import { THUMBNAIL_DIR } from 'src/cores/storage.core'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; +import { userStub } from 'test/fixtures/user.stub'; export const libraryStub = { uploadLibrary1: Object.freeze({ diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index ad9a4baf7..5070586ac 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -1,4 +1,4 @@ -import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from '@app/domain'; +import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.interface'; const probeStubDefaultFormat: VideoFormat = { formatName: 'mov,mp4,m4a,3gp,3g2,mj2', diff --git a/server/test/fixtures/memory.stub.ts b/server/test/fixtures/memory.stub.ts new file mode 100644 index 000000000..bb84a8f1d --- /dev/null +++ b/server/test/fixtures/memory.stub.ts @@ -0,0 +1,30 @@ +import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { userStub } from 'test/fixtures/user.stub'; + +export const memoryStub = { + empty: { + id: 'memoryEmpty', + createdAt: new Date(), + updatedAt: new Date(), + memoryAt: new Date(2024), + ownerId: userStub.admin.id, + owner: userStub.admin, + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + isSaved: false, + assets: [], + }, + memory1: { + id: 'memory1', + createdAt: new Date(), + updatedAt: new Date(), + memoryAt: new Date(2024), + ownerId: userStub.admin.id, + owner: userStub.admin, + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + isSaved: false, + assets: [assetStub.image1], + }, +}; diff --git a/server/test/fixtures/partner.stub.ts b/server/test/fixtures/partner.stub.ts index 05c1c67d6..4e5643bc1 100644 --- a/server/test/fixtures/partner.stub.ts +++ b/server/test/fixtures/partner.stub.ts @@ -1,5 +1,5 @@ -import { PartnerEntity } from '@app/infra/entities'; -import { userStub } from './user.stub'; +import { PartnerEntity } from 'src/entities/partner.entity'; +import { userStub } from 'test/fixtures/user.stub'; export const partnerStub = { adminToUser1: Object.freeze({ diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index ad83d6800..5e5a2214e 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -1,5 +1,5 @@ -import { PersonEntity } from '@app/infra/entities'; -import { userStub } from './user.stub'; +import { PersonEntity } from 'src/entities/person.entity'; +import { userStub } from 'test/fixtures/user.stub'; export const personStub = { noName: Object.freeze({ diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 109f05190..ccd76c328 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,9 +1,16 @@ -import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain'; -import { AssetOrder, AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities'; -import { assetStub } from './asset.stub'; -import { authStub } from './auth.stub'; -import { libraryStub } from './library.stub'; -import { userStub } from './user.stub'; +import { AlbumResponseDto } from 'src/dtos/album.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ExifResponseDto } from 'src/dtos/exif.dto'; +import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; +import { mapUser } from 'src/dtos/user.dto'; +import { AssetOrder } from 'src/entities/album.entity'; +import { AssetType } from 'src/entities/asset.entity'; +import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { libraryStub } from 'test/fixtures/library.stub'; +import { userStub } from 'test/fixtures/user.stub'; const today = new Date(); const tomorrow = new Date(); @@ -192,7 +199,7 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - resizePath: '', + previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -212,7 +219,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - webpPath: '', + thumbnailPath: '', thumbhash: null, encodedVideoPath: '', duration: null, diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts index 9f9f02144..b557644ef 100644 --- a/server/test/fixtures/system-config.stub.ts +++ b/server/test/fixtures/system-config.stub.ts @@ -1,4 +1,4 @@ -import { SystemConfigEntity, SystemConfigKey } from '@app/infra/entities'; +import { SystemConfigEntity, SystemConfigKey } from 'src/entities/system-config.entity'; export const systemConfigStub: Record = { defaults: [], diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index cffae0032..537c65db4 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,6 +1,6 @@ -import { TagResponseDto } from '@app/domain'; -import { TagEntity, TagType } from '@app/infra/entities'; -import { userStub } from './user.stub'; +import { TagResponseDto } from 'src/dtos/tag.dto'; +import { TagEntity, TagType } from 'src/entities/tag.entity'; +import { userStub } from 'test/fixtures/user.stub'; export const tagStub = { tag1: Object.freeze({ diff --git a/server/test/fixtures/user-token.stub.ts b/server/test/fixtures/user-token.stub.ts index 975318e21..2f6fcc0cd 100644 --- a/server/test/fixtures/user-token.stub.ts +++ b/server/test/fixtures/user-token.stub.ts @@ -1,5 +1,5 @@ -import { UserTokenEntity } from '@app/infra/entities'; -import { userStub } from './user.stub'; +import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { userStub } from 'test/fixtures/user.stub'; export const userTokenStub = { userToken: Object.freeze({ diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index e0d9113c6..5cf5acfc3 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,5 +1,5 @@ -import { UserAvatarColor, UserEntity } from '@app/infra/entities'; -import { authStub } from './auth.stub'; +import { UserAvatarColor, UserEntity } from 'src/entities/user.entity'; +import { authStub } from 'test/fixtures/auth.stub'; export const userDto = { user1: { diff --git a/server/test/index.ts b/server/test/index.ts deleted file mode 100644 index 784eeeb35..000000000 --- a/server/test/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './fixtures'; -export * from './repositories'; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index e10dd7d9a..fe7de7c83 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,4 +1,5 @@ -import { AccessCore, IAccessRepository } from '@app/domain'; +import { AccessCore } from 'src/cores/access.core'; +import { IAccessRepository } from 'src/interfaces/access.interface'; export interface IAccessRepositoryMock { activity: jest.Mocked; @@ -7,6 +8,7 @@ export interface IAccessRepositoryMock { authDevice: jest.Mocked; library: jest.Mocked; timeline: jest.Mocked; + memory: jest.Mocked; person: jest.Mocked; partner: jest.Mocked; } @@ -48,6 +50,10 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, + memory: { + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + }, + person: { checkFaceOwnerAccess: jest.fn().mockResolvedValue(new Set()), checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts index 349fa4636..276b57c6c 100644 --- a/server/test/repositories/activity.repository.mock.ts +++ b/server/test/repositories/activity.repository.mock.ts @@ -1,4 +1,4 @@ -import { IActivityRepository } from '@app/domain'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; export const newActivityRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index 36c3afb29..38db70e4b 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAlbumRepository } from '@app/domain'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; export const newAlbumRepositoryMock = (): jest.Mocked => { return { @@ -14,9 +14,9 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { softDeleteAll: jest.fn(), deleteAll: jest.fn(), getAll: jest.fn(), - addAssets: jest.fn(), + addAssetIds: jest.fn(), removeAsset: jest.fn(), - removeAssets: jest.fn(), + removeAssetIds: jest.fn(), getAssetIds: jest.fn(), hasAsset: jest.fn(), create: jest.fn(), diff --git a/server/test/repositories/api-key.repository.mock.ts b/server/test/repositories/api-key.repository.mock.ts index 5688978e7..32b8388a3 100644 --- a/server/test/repositories/api-key.repository.mock.ts +++ b/server/test/repositories/api-key.repository.mock.ts @@ -1,4 +1,4 @@ -import { IKeyRepository } from '@app/domain'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; export const newKeyRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/asset-stack.repository.mock.ts b/server/test/repositories/asset-stack.repository.mock.ts index d87f0316f..76ada96cd 100644 --- a/server/test/repositories/asset-stack.repository.mock.ts +++ b/server/test/repositories/asset-stack.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAssetStackRepository } from '@app/domain'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; export const newAssetStackRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index b291b7183..67770cd93 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -1,11 +1,10 @@ -import { IAssetRepository } from '@app/domain'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; export const newAssetRepositoryMock = (): jest.Mocked => { return { create: jest.fn(), upsertExif: jest.fn(), upsertJobStatus: jest.fn(), - getByDate: jest.fn(), getByDayOfYear: jest.fn(), getByIds: jest.fn().mockResolvedValue([]), getByIdsWithAllRelations: jest.fn().mockResolvedValue([]), @@ -24,7 +23,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getLibraryAssetPaths: jest.fn(), getByLibraryIdAndOriginalPath: jest.fn(), deleteAll: jest.fn(), - save: jest.fn(), + update: jest.fn(), remove: jest.fn(), findLivePhotoMatch: jest.fn(), getMapMarkers: jest.fn(), diff --git a/server/test/repositories/audit.repository.mock.ts b/server/test/repositories/audit.repository.mock.ts index bd1a4b815..9e4adf560 100644 --- a/server/test/repositories/audit.repository.mock.ts +++ b/server/test/repositories/audit.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAuditRepository } from '@app/domain'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; export const newAuditRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/communication.repository.mock.ts b/server/test/repositories/communication.repository.mock.ts deleted file mode 100644 index e98e0a68f..000000000 --- a/server/test/repositories/communication.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ICommunicationRepository } from '@app/domain'; - -export const newCommunicationRepositoryMock = (): jest.Mocked => { - return { - send: jest.fn(), - broadcast: jest.fn(), - on: jest.fn(), - sendServerEvent: jest.fn(), - emit: jest.fn(), - emitAsync: jest.fn(), - }; -}; diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts index 52f438453..8d13814db 100644 --- a/server/test/repositories/crypto.repository.mock.ts +++ b/server/test/repositories/crypto.repository.mock.ts @@ -1,4 +1,4 @@ -import { ICryptoRepository } from '@app/domain'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; export const newCryptoRepositoryMock = (): jest.Mocked => { return { @@ -9,5 +9,6 @@ export const newCryptoRepositoryMock = (): jest.Mocked => { hashSha256: jest.fn().mockImplementation((input) => `${input} (hashed)`), hashSha1: jest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), hashFile: jest.fn().mockImplementation((input) => `${input} (file-hashed)`), + newPassword: jest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')), }; }; diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index 19e2df17a..704189571 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -1,4 +1,5 @@ -import { IDatabaseRepository, Version } from '@app/domain'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { Version } from 'src/utils/version'; export const newDatabaseRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts new file mode 100644 index 000000000..b21d4a59e --- /dev/null +++ b/server/test/repositories/event.repository.mock.ts @@ -0,0 +1,10 @@ +import { IEventRepository } from 'src/interfaces/event.interface'; + +export const newEventRepositoryMock = (): jest.Mocked => { + return { + clientSend: jest.fn(), + clientBroadcast: jest.fn(), + serverSend: jest.fn(), + serverSendAsync: jest.fn(), + }; +}; diff --git a/server/test/repositories/index.ts b/server/test/repositories/index.ts deleted file mode 100644 index 90fd1326b..000000000 --- a/server/test/repositories/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -export * from './access.repository.mock'; -export * from './album.repository.mock'; -export * from './api-key.repository.mock'; -export * from './asset-stack.repository.mock'; -export * from './asset.repository.mock'; -export * from './audit.repository.mock'; -export * from './communication.repository.mock'; -export * from './crypto.repository.mock'; -export * from './database.repository.mock'; -export * from './job.repository.mock'; -export * from './library.repository.mock'; -export * from './machine-learning.repository.mock'; -export * from './media.repository.mock'; -export * from './metadata.repository.mock'; -export * from './move.repository.mock'; -export * from './partner.repository.mock'; -export * from './person.repository.mock'; -export * from './search.repository.mock'; -export * from './shared-link.repository.mock'; -export * from './storage.repository.mock'; -export * from './system-config.repository.mock'; -export * from './system-info.repository.mock'; -export * from './system-metadata.repository.mock'; -export * from './tag.repository.mock'; -export * from './user-token.repository.mock'; -export * from './user.repository.mock'; diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index 5967c3ce2..9cd21fe87 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -1,4 +1,4 @@ -import { IJobRepository } from '@app/domain'; +import { IJobRepository } from 'src/interfaces/job.interface'; export const newJobRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts index 740f4c483..6cdfb38f4 100644 --- a/server/test/repositories/library.repository.mock.ts +++ b/server/test/repositories/library.repository.mock.ts @@ -1,4 +1,4 @@ -import { ILibraryRepository } from '@app/domain'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; export const newLibraryRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/machine-learning.repository.mock.ts b/server/test/repositories/machine-learning.repository.mock.ts index 3538b7893..bc35b4c85 100644 --- a/server/test/repositories/machine-learning.repository.mock.ts +++ b/server/test/repositories/machine-learning.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMachineLearningRepository } from '@app/domain'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; export const newMachineLearningRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 74c4a5d7a..b904766ea 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMediaRepository } from '@app/domain'; +import { IMediaRepository } from 'src/interfaces/media.interface'; export const newMediaRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/memory.repository.mock.ts b/server/test/repositories/memory.repository.mock.ts new file mode 100644 index 000000000..85b17a198 --- /dev/null +++ b/server/test/repositories/memory.repository.mock.ts @@ -0,0 +1,14 @@ +import { IMemoryRepository } from 'src/interfaces/memory.interface'; + +export const newMemoryRepositoryMock = (): jest.Mocked => { + return { + search: jest.fn().mockResolvedValue([]), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + getAssetIds: jest.fn().mockResolvedValue(new Set()), + addAssetIds: jest.fn(), + removeAssetIds: jest.fn(), + }; +}; diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index e47120ac9..ec21ab8c1 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMetadataRepository } from '@app/domain'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; export const newMetadataRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/metric.repository.mock.ts b/server/test/repositories/metric.repository.mock.ts new file mode 100644 index 000000000..383845d34 --- /dev/null +++ b/server/test/repositories/metric.repository.mock.ts @@ -0,0 +1,30 @@ +import { IMetricRepository } from 'src/interfaces/metric.interface'; + +export const newMetricRepositoryMock = (): jest.Mocked => { + return { + api: { + addToCounter: jest.fn(), + addToGauge: jest.fn(), + addToHistogram: jest.fn(), + configure: jest.fn(), + }, + host: { + addToCounter: jest.fn(), + addToGauge: jest.fn(), + addToHistogram: jest.fn(), + configure: jest.fn(), + }, + jobs: { + addToCounter: jest.fn(), + addToGauge: jest.fn(), + addToHistogram: jest.fn(), + configure: jest.fn(), + }, + repo: { + addToCounter: jest.fn(), + addToGauge: jest.fn(), + addToHistogram: jest.fn(), + configure: jest.fn(), + }, + }; +}; diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts index e14b0640b..b7adec2a7 100644 --- a/server/test/repositories/move.repository.mock.ts +++ b/server/test/repositories/move.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMoveRepository } from '@app/domain'; +import { IMoveRepository } from 'src/interfaces/move.interface'; export const newMoveRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts index 1e839ae4f..04370730b 100644 --- a/server/test/repositories/partner.repository.mock.ts +++ b/server/test/repositories/partner.repository.mock.ts @@ -1,4 +1,4 @@ -import { IPartnerRepository } from '@app/domain'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; export const newPartnerRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 2a1ccdfe5..5b94fbc3d 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -1,4 +1,4 @@ -import { IPersonRepository } from '@app/domain'; +import { IPersonRepository } from 'src/interfaces/person.interface'; export const newPersonRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index 5912d7745..24e648ee2 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISearchRepository } from '@app/domain'; +import { ISearchRepository } from 'src/interfaces/search.interface'; export const newSearchRepositoryMock = (): jest.Mocked => { return { @@ -8,6 +8,7 @@ export const newSearchRepositoryMock = (): jest.Mocked => { searchFaces: jest.fn(), upsert: jest.fn(), searchPlaces: jest.fn(), + getAssetsByCity: jest.fn(), deleteAllSearchEmbeddings: jest.fn(), }; }; diff --git a/server/test/repositories/shared-link.repository.mock.ts b/server/test/repositories/shared-link.repository.mock.ts index fb34b0ad7..2fcaf7aee 100644 --- a/server/test/repositories/shared-link.repository.mock.ts +++ b/server/test/repositories/shared-link.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISharedLinkRepository } from '@app/domain'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; export const newSharedLinkRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index a8ffbf410..d5049999c 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -1,5 +1,6 @@ -import { IStorageRepository, StorageCore, StorageEventType, WatchEvents } from '@app/domain'; import { WatchOptions } from 'chokidar'; +import { StorageCore } from 'src/cores/storage.core'; +import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.interface'; interface MockWatcherOptions { items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; diff --git a/server/test/repositories/system-config.repository.mock.ts b/server/test/repositories/system-config.repository.mock.ts index 3be69f267..0ef11ce18 100644 --- a/server/test/repositories/system-config.repository.mock.ts +++ b/server/test/repositories/system-config.repository.mock.ts @@ -1,4 +1,5 @@ -import { ISystemConfigRepository, SystemConfigCore } from '@app/domain'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; export const newSystemConfigRepositoryMock = (reset = true): jest.Mocked => { if (reset) { diff --git a/server/test/repositories/system-info.repository.mock.ts b/server/test/repositories/system-info.repository.mock.ts index 14c52a6b7..bdc11f9d6 100644 --- a/server/test/repositories/system-info.repository.mock.ts +++ b/server/test/repositories/system-info.repository.mock.ts @@ -1,4 +1,4 @@ -import { IServerInfoRepository } from '@app/domain'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; export const newServerInfoRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts index fc4207da6..5ffc5dd89 100644 --- a/server/test/repositories/system-metadata.repository.mock.ts +++ b/server/test/repositories/system-metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISystemMetadataRepository } from '@app/domain'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; export const newSystemMetadataRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index cb1e05a09..0c31c546c 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITagRepository } from '@app/domain'; +import { ITagRepository } from 'src/interfaces/tag.interface'; export const newTagRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/user-token.repository.mock.ts b/server/test/repositories/user-token.repository.mock.ts index 9d1bacf1c..b3fa7e73f 100644 --- a/server/test/repositories/user-token.repository.mock.ts +++ b/server/test/repositories/user-token.repository.mock.ts @@ -1,4 +1,4 @@ -import { IUserTokenRepository } from '@app/domain'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; export const newUserTokenRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 402b90ead..80d9a4cfd 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -1,4 +1,5 @@ -import { IUserRepository, UserCore } from '@app/domain'; +import { UserCore } from 'src/cores/user.core'; +import { IUserRepository } from 'src/interfaces/user.interface'; export const newUserRepositoryMock = (reset = true): jest.Mocked => { if (reset) { diff --git a/server/src/test-utils/utils.ts b/server/test/utils.ts similarity index 80% rename from server/src/test-utils/utils.ts rename to server/test/utils.ts index cf9822295..c7732eabc 100644 --- a/server/src/test-utils/utils.ts +++ b/server/test/utils.ts @@ -1,18 +1,19 @@ -import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName, StorageEventType } from '@app/domain'; -import { AppModule } from '@app/immich'; -import { InfraModule, InfraTestModule, dataSource } from '@app/infra'; -import { MediaRepository } from '@app/infra/repositories'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { DateTime } from 'luxon'; -import * as fs from 'node:fs'; +import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { EventEmitter } from 'node:stream'; -import { Server } from 'node:tls'; +import { AppTestModule } from 'src/app.module'; +import { dataSource } from 'src/database.config'; +import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { StorageEventType } from 'src/interfaces/storage.interface'; +import { MediaRepository } from 'src/repositories/media.repository'; +import { ApiService } from 'src/services/api.service'; +import { MicroservicesService } from 'src/services/microservices.service'; import { EntityTarget, ObjectLiteral } from 'typeorm'; -import { AppService } from '../immich/app.service'; -import { AppService as MicroAppService } from '../microservices/app.service'; export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string; export const IMMICH_TEST_ASSET_TEMP_PATH = join(tmpdir(), 'immich'); @@ -101,12 +102,7 @@ let app: INestApplication; export const testApp = { create: async (): Promise => { - const moduleFixture = await Test.createTestingModule({ - imports: [AppModule], - providers: [AppService, MicroAppService], - }) - .overrideModule(InfraModule) - .useModule(InfraTestModule) + const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] }) .overrideProvider(IJobRepository) .useClass(JobMock) .overrideProvider(IMediaRepository) @@ -114,14 +110,10 @@ export const testApp = { .compile(); app = await moduleFixture.createNestApplication().init(); - await app.listen(0); + await app.get(ApiService).init(); await db.reset(); - await app.get(AppService).init(); - await app.get(MicroAppService).init(); - - const port = app.getHttpServer().address().port; - const protocol = app instanceof Server ? 'https' : 'http'; - process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port; + await app.get(ApiService).init(); + await app.get(MicroservicesService).init(); return app; }, @@ -131,7 +123,7 @@ export const testApp = { get: (member: any) => app.get(member), teardown: async () => { if (app) { - await app.get(MicroAppService).teardown(); + await app.get(MicroservicesService).teardown(); await app.close(); } await db.disconnect(); diff --git a/server/tsconfig.json b/server/tsconfig.json index 6d89fe708..ce3edda39 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -17,16 +17,10 @@ "esModuleInterop": true, "preserveWatchOutput": true, "baseUrl": "./", - "paths": { - "@test": ["test"], - "@test/*": ["test/*"], - "@app/immich": ["src/immich"], - "@app/immich/*": ["src/immich/*"], - "@app/infra": ["src/infra"], - "@app/infra/*": ["src/infra/*"], - "@app/domain": ["src/domain"], - "@app/domain/*": ["src/domain/*"], - }, }, - "exclude": ["dist", "node_modules", "upload"], -} + "exclude": [ + "dist", + "node_modules", + "upload" + ], +} \ No newline at end of file diff --git a/web/.eslintignore b/web/.eslintignore index 38972655f..f944e33c4 100644 --- a/web/.eslintignore +++ b/web/.eslintignore @@ -11,3 +11,4 @@ node_modules pnpm-lock.yaml package-lock.json yarn.lock +svelte.config.js diff --git a/web/Dockerfile b/web/Dockerfile index 27d206e92..c59b6c716 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:iron-alpine3.18@sha256:a02826c7340c37a29179152723190bcc3044f933c925f3c2d78abb20f794de3f +FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef RUN apk add --no-cache tini USER node diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..e9693ceb0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,5 @@ +# Immich web project + +This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing). + +When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project). diff --git a/web/package-lock.json b/web/package-lock.json index 899ec9156..a855def3b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.98.2", + "version": "1.100.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" @@ -459,9 +459,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -475,9 +475,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -491,9 +491,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -507,9 +507,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -523,9 +523,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -539,9 +539,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -571,9 +571,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -587,9 +587,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -603,9 +603,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -619,9 +619,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -635,9 +635,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -651,9 +651,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -667,9 +667,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -683,9 +683,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -699,9 +699,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -715,9 +715,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -731,9 +731,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -747,9 +747,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -763,9 +763,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -779,9 +779,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -795,9 +795,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -811,9 +811,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -1456,9 +1456,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1625,9 +1625,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", - "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", "cpu": [ "arm" ], @@ -1638,9 +1638,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", - "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", "cpu": [ "arm64" ], @@ -1651,9 +1651,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", - "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ "arm64" ], @@ -1664,9 +1664,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", - "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", "cpu": [ "x64" ], @@ -1677,9 +1677,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", - "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", "cpu": [ "arm" ], @@ -1690,9 +1690,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", - "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", "cpu": [ "arm64" ], @@ -1703,9 +1703,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", - "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", "cpu": [ "arm64" ], @@ -1716,9 +1716,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", - "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", "cpu": [ "riscv64" ], @@ -1729,9 +1729,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", - "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", "cpu": [ "x64" ], @@ -1742,9 +1742,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", - "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", "cpu": [ "x64" ], @@ -1755,9 +1755,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", - "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", "cpu": [ "arm64" ], @@ -1768,9 +1768,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", - "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", "cpu": [ "ia32" ], @@ -1781,9 +1781,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", - "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -1814,9 +1814,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.1.8.tgz", - "integrity": "sha512-0cLVR9KiO0/t3VVm64OM7bPHTkdaT2aaz1rwoAhao+EBXR3vMvLoYXLHvz8o9/552PSV8G844RkH7qkGc3YAiQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.1.9.tgz", + "integrity": "sha512-gUgaiG88P6moWcxZx4YrzMhAlw1TgggKRp7n9gdfCREDeXHysCd1l9GpQR3sh109SM3rNlkiaAzt+iPLT0aG1w==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -1825,9 +1825,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz", - "integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.4.tgz", + "integrity": "sha512-eDxK2d4EGzk99QsZNoPXe7jlzA5EGqfcCpUwZ912bhnalsZ2ZsG5wGRthkydupVjYyqdmzEanVKFhLxU2vkPSQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2264,16 +2264,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2282,7 +2282,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2332,19 +2332,19 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2360,16 +2360,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2377,18 +2377,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2404,12 +2404,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2417,13 +2417,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2432,7 +2432,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2502,21 +2502,21 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2560,16 +2560,16 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2583,14 +2583,14 @@ "dev": true }, "node_modules/@vitest/browser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz", - "integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.4.0.tgz", + "integrity": "sha512-kC44DzuqPZZrqe2P7SX2a3zHDAt919WtpkUMAxzv9eP5uPfVXtpk2Ipms2NXJGY5190aJc1uY+ambfJ3rwDJRA==", "dev": true, "optional": true, "peer": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.4.0", "magic-string": "^0.30.5", "sirv": "^2.0.4" }, @@ -2599,7 +2599,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "1.3.1", + "vitest": "1.4.0", "webdriverio": "*" }, "peerDependenciesMeta": { @@ -2615,9 +2615,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", - "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz", + "integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -2625,12 +2625,13 @@ "debug": "^4.3.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^4.0.1", + "istanbul-lib-source-maps": "^5.0.4", "istanbul-reports": "^3.1.6", "magic-string": "^0.30.5", "magicast": "^0.3.3", "picocolors": "^1.0.0", "std-env": "^3.5.0", + "strip-literal": "^2.0.0", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.2.0" }, @@ -2638,17 +2639,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.3.1" + "vitest": "1.4.0" } }, "node_modules/@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", + "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", "dev": true, "dependencies": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "chai": "^4.3.10" }, "funding": { @@ -2656,12 +2657,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", + "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", "dev": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.4.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2697,9 +2698,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", + "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2743,9 +2744,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", + "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2755,9 +2756,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2802,9 +2803,9 @@ "dev": true }, "node_modules/@zoom-image/core": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz", - "integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==", + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.2.tgz", + "integrity": "sha512-Nqg/JrvtaScXJ16L7dcPQjun3F0enajULtokYWy13VtETqqBOBqKDa3feTJH7JXrYvEs/w6e4AU56UvzGG1wXA==", "dependencies": { "@namnode/store": "^0.1.0" }, @@ -2814,11 +2815,11 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz", - "integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.8.tgz", + "integrity": "sha512-z+xCyMHIeTmSYZYdDde/EAz08odlBWMv6jmHOcz95DMt3sJ/+vlVtUEMCzZfuK2KqV8v59EKZHAeutMzTx3QPg==", "dependencies": { - "@zoom-image/core": "0.33.0" + "@zoom-image/core": "0.33.2" }, "funding": { "type": "github", @@ -3003,9 +3004,9 @@ "peer": true }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -3023,7 +3024,7 @@ ], "dependencies": { "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -3255,9 +3256,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "version": "1.0.30001600", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", + "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", "dev": true, "funding": [ { @@ -3963,9 +3964,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -3975,29 +3976,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { @@ -5649,14 +5650,14 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", "dev": true, "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" @@ -5676,9 +5677,9 @@ } }, "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -6714,9 +6715,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -6735,7 +6736,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7289,9 +7290,9 @@ } }, "node_modules/rollup": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", - "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -7304,19 +7305,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.5", - "@rollup/rollup-android-arm64": "4.9.5", - "@rollup/rollup-darwin-arm64": "4.9.5", - "@rollup/rollup-darwin-x64": "4.9.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", - "@rollup/rollup-linux-arm64-gnu": "4.9.5", - "@rollup/rollup-linux-arm64-musl": "4.9.5", - "@rollup/rollup-linux-riscv64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-musl": "4.9.5", - "@rollup/rollup-win32-arm64-msvc": "4.9.5", - "@rollup/rollup-win32-ia32-msvc": "4.9.5", - "@rollup/rollup-win32-x64-msvc": "4.9.5", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, @@ -7671,9 +7672,9 @@ } }, "node_modules/socket.io-client": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", - "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -7752,9 +7753,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -8038,9 +8039,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.6.tgz", - "integrity": "sha512-b9q9rOHOMYF3U8XllK7LmXTq1LeWQ98waGfEJzrFutViadkNl1tgdEtxIQ8yuPx+VQ4l7YrknYol+0lfZocaZw==", + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.8.tgz", + "integrity": "sha512-rhXU7YCDtL+lq2gCqfJDXKTxJfSsCgcd08d7VWBFxTw6IWIbMWSaASbAOD3N0VV9TYSSLUqEBiratLd8WxAJJA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -8222,9 +8223,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -8235,7 +8236,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -8294,19 +8295,25 @@ } }, "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "dev": true, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -8520,9 +8527,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -8670,14 +8677,14 @@ } }, "node_modules/vite": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz", - "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", + "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.36", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -8738,9 +8745,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", + "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -8774,16 +8781,16 @@ } }, "node_modules/vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", + "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", "dev": true, "dependencies": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/expect": "1.4.0", + "@vitest/runner": "1.4.0", + "@vitest/snapshot": "1.4.0", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -8797,7 +8804,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.3.1", + "vite-node": "1.4.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -8812,8 +8819,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.1", - "@vitest/ui": "1.3.1", + "@vitest/browser": "1.4.0", + "@vitest/ui": "1.4.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/web/package.json b/web/package.json index 4714b09b0..c9e08c85f 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.98.2", + "version": "1.100.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", diff --git a/web/src/app.css b/web/src/app.css index c361d890c..9db440e46 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -29,6 +29,7 @@ src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); font-weight: 1 999; font-style: normal; + ascent-override: 100%; } @font-face { @@ -36,6 +37,7 @@ src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); font-weight: 1 999; font-style: monospace; + ascent-override: 100%; } @font-face { diff --git a/web/src/lib/__mocks__/sdk.mock.ts b/web/src/lib/__mocks__/sdk.mock.ts new file mode 100644 index 000000000..a3e6f0f4d --- /dev/null +++ b/web/src/lib/__mocks__/sdk.mock.ts @@ -0,0 +1,18 @@ +import sdk from '@immich/sdk'; +import type { Mock, MockedObject } from 'vitest'; + +vi.mock('@immich/sdk', async (originalImport) => { + const module = await originalImport(); + + const mocks: Record = {}; + for (const [key, value] of Object.entries(module)) { + if (typeof value === 'function') { + mocks[key] = vi.fn(); + } + } + + const mock = { ...module, ...mocks }; + return { ...mock, default: mock }; +}); + +export const sdkMock = sdk as MockedObject; diff --git a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte similarity index 75% rename from web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte rename to web/src/lib/components/admin-page/settings/image/image-settings.svelte index 8e2936b55..dcf59936d 100644 --- a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -25,10 +25,10 @@

(config.thumbnail.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} - isEdited={config.thumbnail.colorspace !== savedConfig.thumbnail.colorspace} + checked={config.image.colorspace === Colorspace.P3} + on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} + isEdited={config.image.colorspace !== savedConfig.image.colorspace} />
dispatch('reset', { ...detail, configKeys: ['thumbnail'] })} - on:save={() => dispatch('save', { thumbnail: config.thumbnail })} + on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['image'] })} + on:save={() => dispatch('save', { image: config.image })} showResetToDefault={!isEqual(savedConfig, defaultConfig)} {disabled} /> diff --git a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte index fbda787bc..b6f561c3d 100644 --- a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte +++ b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte @@ -21,7 +21,7 @@
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index b273271ce..6c6dc98f7 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -1,18 +1,11 @@ import { createObjectURLMock } from '$lib/__mocks__/jsdom-url.mock'; -import sdk, { ThumbnailFormat } from '@immich/sdk'; +import { sdkMock } from '$lib/__mocks__/sdk.mock'; +import { ThumbnailFormat } from '@immich/sdk'; import { albumFactory } from '@test-data'; import '@testing-library/jest-dom'; import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte'; -import type { MockedObject } from 'vitest'; import AlbumCard from '../album-card.svelte'; -vi.mock('@immich/sdk', async (originalImport) => { - const module = await originalImport(); - const mock = { ...module, getAssetThumbnail: vi.fn() }; - return { ...mock, default: mock }; -}); - -const sdkMock: MockedObject = sdk as MockedObject; const onShowContextMenu = vi.fn(); describe('AlbumCard component', () => { diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index d5f816047..5118daf06 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -61,7 +61,7 @@

Options

- dispatch('close')} /> + dispatch('close')} />
diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte index a58dbc0e3..a5f2c9276 100644 --- a/web/src/lib/components/album-page/albums-controls.svelte +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -2,7 +2,7 @@ import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import Dropdown from '$lib/components/elements/dropdown.svelte'; import Icon from '$lib/components/elements/icon.svelte'; - import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store'; + import { AlbumFilter, AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store'; import { mdiArrowDownThin, mdiArrowUpThin, @@ -12,6 +12,7 @@ } from '@mdi/js'; import { sortByOptions, type Sort, handleCreateAlbum } from '$lib/components/album-page/albums-list.svelte'; import SearchBar from '$lib/components/elements/search-bar.svelte'; + import GroupTab from '$lib/components/elements/group-tab.svelte'; export let searchAlbum: string; @@ -25,13 +26,20 @@ }; -