From afc6e91c66e15bc2530aa2ade978113b6f11e37c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jan 2025 09:22:27 -0600 Subject: [PATCH 01/12] fix(web): No EXIF info on stack navigation (#15533) * fix(web): No EXIF info on stack navigation * fix(web): No EXIF info on stack navigation * add exif info to get stack query * e2e test --- e2e/src/api/specs/stack.e2e-spec.ts | 27 +++++++++++++++ server/src/queries/stack.repository.sql | 37 ++++++++++++++++++--- server/src/repositories/stack.repository.ts | 7 +++- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/e2e/src/api/specs/stack.e2e-spec.ts b/e2e/src/api/specs/stack.e2e-spec.ts index 36b600bec9..91dd0d2a8e 100644 --- a/e2e/src/api/specs/stack.e2e-spec.ts +++ b/e2e/src/api/specs/stack.e2e-spec.ts @@ -172,4 +172,31 @@ describe('/stacks', () => { ); }); }); + + describe('GET /stacks/:id', () => { + it('should include exifInfo in stack assets', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const stack = await utils.createStack(user1.accessToken, [asset1.id, asset2.id]); + + const { status, body } = await request(app) + .get(`/stacks/${stack.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + id: stack.id, + primaryAssetId: asset1.id, + assets: expect.arrayContaining([ + expect.objectContaining({ id: asset1.id, exifInfo: expect.any(Object) }), + expect.objectContaining({ id: asset2.id, exifInfo: expect.any(Object) }), + ]), + }), + ); + }); + }); }); diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 54f86c94af..0fd1b233be 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -9,9 +9,18 @@ select from ( select - * + "assets".*, + to_json("exifInfo") as "exifInfo" from "assets" + inner join lateral ( + select + "exif".* + from + "exif" + where + "exif"."assetId" = "assets"."id" + ) as "exifInfo" on true where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" @@ -31,7 +40,7 @@ select from ( select - *, + "assets".*, ( select coalesce(json_agg(agg), '[]') @@ -45,9 +54,18 @@ select where "tag_asset"."assetsId" = "assets"."id" ) as agg - ) as "tags" + ) as "tags", + to_json("exifInfo") as "exifInfo" from "assets" + inner join lateral ( + select + "exif".* + from + "exif" + where + "exif"."assetId" = "assets"."id" + ) as "exifInfo" on true where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" @@ -67,7 +85,7 @@ select from ( select - *, + "assets".*, ( select coalesce(json_agg(agg), '[]') @@ -81,9 +99,18 @@ select where "tag_asset"."assetsId" = "assets"."id" ) as agg - ) as "tags" + ) as "tags", + to_json("exifInfo") as "exifInfo" from "assets" + inner join lateral ( + select + "exif".* + from + "exif" + where + "exif"."assetId" = "assets"."id" + ) as "exifInfo" on true where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index 6a80c1f59c..018d7e77a4 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -12,7 +12,11 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) return jsonArrayFrom( eb .selectFrom('assets') - .selectAll() + .selectAll('assets') + .innerJoinLateral( + (eb) => eb.selectFrom('exif').selectAll('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'), + (join) => join.onTrue(), + ) .$if(withTags, (eb) => eb.select((eb) => jsonArrayFrom( @@ -24,6 +28,7 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) ).as('tags'), ), ) + .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) .where('assets.deletedAt', 'is', null) .whereRef('assets.stackId', '=', 'asset_stack.id'), ).as('assets'); From f32c5d97cde40f7b13b4ab18f32f29f4941805af Mon Sep 17 00:00:00 2001 From: David Baxter Date: Thu, 23 Jan 2025 07:34:36 -0800 Subject: [PATCH 02/12] feat(web): Show lens model in the asset viewer detail panel (#15460) * Adds lens details to the asset viewer * Update lens detail search links --------- Co-authored-by: Alex Tran --- i18n/en.json | 2 ++ .../asset-viewer/detail-panel.svelte | 32 ++++++++++++++++++- .../[[assetId=id]]/+page.svelte | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 737ec2704d..ad48a96991 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -822,6 +822,7 @@ "latest_version": "Latest Version", "latitude": "Latitude", "leave": "Leave", + "lens_model": "Lens model", "let_others_respond": "Let others respond", "level": "Level", "library": "Library", @@ -1113,6 +1114,7 @@ "search_camera_model": "Search camera model...", "search_city": "Search city...", "search_country": "Search country...", + "search_for": "Search for", "search_for_existing_person": "Search for existing person", "search_no_people": "No people", "search_no_people_named": "No people named \"{name}\"", diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index fde4efae95..cdc00e247f 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -45,6 +45,7 @@ import UserAvatar from '../shared-components/user-avatar.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte'; + import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; interface Props { asset: AssetResponseDto; @@ -410,7 +411,36 @@
-

{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}

+ {#if asset.exifInfo?.make || asset.exifInfo?.model} +

+ + {asset.exifInfo.make || ''} + {asset.exifInfo.model || ''} + +

+ {/if} + + {#if asset.exifInfo?.lensModel} + + {/if} +
{#if asset.exifInfo?.fNumber}

ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}

diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index fe4a7a6612..97d0cacdce 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -192,6 +192,7 @@ state: $t('state'), make: $t('camera_brand'), model: $t('camera_model'), + lensModel: $t('lens_model'), personIds: $t('people'), originalFileName: $t('file_name'), }; From a47aa863920438ba8be7a4153034e98ef08a4d5c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jan 2025 09:45:07 -0600 Subject: [PATCH 03/12] chore: minor form bottom padding increase (#15558) --- web/src/lib/components/layouts/AuthPageLayout.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 78ff67cfcb..c6ccec0f51 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -18,8 +18,11 @@ {title} - - {@render children?.()} - + +
+ + {@render children?.()} + +
From 49a16045bd70856b5268777a9b942131b6952f77 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:23:47 +0000 Subject: [PATCH 04/12] chore: version v1.125.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 8 ++++---- web/package.json | 2 +- 18 files changed, 32 insertions(+), 28 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index f2cba97088..c52d5f0e0d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.40", + "version": "2.2.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.40", + "version": "2.2.41", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.124.2", + "version": "1.125.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 4296651eed..f3f9b767ad 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.40", + "version": "2.2.41", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 313d5ad221..2ace6c0a5f 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.0", + "url": "https://v1.125.0.archive.immich.app" + }, { "label": "v1.124.2", "url": "https://v1.124.2.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index b80319438d..d0cbb4dbba 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.124.2", + "version": "1.125.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.40", + "version": "2.2.41", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.124.2", + "version": "1.125.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 9f8de74df1..04f0cfb3e8 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.124.2", + "version": "1.125.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e552243d30..648ebc4b28 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.124.2" +version = "1.125.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index c8d80d3224..b06b3761c0 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" => 175, - "android.injected.version.name" => "1.124.2", + "android.injected.version.code" => 176, + "android.injected.version.name" => "1.125.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/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 19b0f50040..ed9cdd9304 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.124.2" + version_number: "1.125.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2a207d52a9..0193e4f799 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.124.2 +- API version: 1.125.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 40b4345992..58e94d8c81 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.124.2+175 +version: 1.125.0+176 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 505a9e93f0..5d18987ba6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.124.2", + "version": "1.125.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index c1ab278404..ee81998a8f 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.124.2", + "version": "1.125.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index b7e1378d8e..a8625ec4e6 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.124.2", + "version": "1.125.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 80c1a667a3..60aec025a4 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.124.2 + * 1.125.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 674dd76a15..31fb98e2d1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.124.2", + "version": "1.125.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 39474753e3..3068df625a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.124.2", + "version": "1.125.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index ac46d9e5d2..5714c183d4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.124.2", + "version": "1.125.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,13 +75,13 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.7", + "@types/node": "^22.10.9", "typescript": "^5.3.3" } }, diff --git a/web/package.json b/web/package.json index bcb8ad0b21..afa5f6e3fd 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.124.2", + "version": "1.125.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 907fed108163e88157a0fae09e2e154fbea6d7a5 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 23 Jan 2025 17:46:56 +0100 Subject: [PATCH 05/12] fix: use push-o-matic to create release (#15562) --- .github/workflows/prepare-release.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index fc03b24d08..9be52f90f0 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -68,10 +68,17 @@ jobs: needs: build_mobile steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + - name: Checkout uses: actions/checkout@v4 with: - token: ${{ secrets.ORG_RELEASE_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} - name: Download APK uses: actions/download-artifact@v4 From 50a2f6193f216d47443e013db72888a7ebe09842 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:52:23 +0000 Subject: [PATCH 06/12] chore: version v1.125.1 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index c52d5f0e0d..f7364aaa21 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.41", + "version": "2.2.42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.41", + "version": "2.2.42", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.0", + "version": "1.125.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index f3f9b767ad..b995d852de 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.41", + "version": "2.2.42", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 2ace6c0a5f..01975e79c1 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.1", + "url": "https://v1.125.1.archive.immich.app" + }, { "label": "v1.125.0", "url": "https://v1.125.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d0cbb4dbba..31b79ce44b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.0", + "version": "1.125.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.41", + "version": "2.2.42", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.0", + "version": "1.125.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 04f0cfb3e8..73b4a2dc29 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.0", + "version": "1.125.1", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 648ebc4b28..a89916dc68 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.0" +version = "1.125.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index b06b3761c0..eec5f8bc88 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" => 176, - "android.injected.version.name" => "1.125.0", + "android.injected.version.code" => 177, + "android.injected.version.name" => "1.125.1", } ) 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/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index ed9cdd9304..c88974a9e5 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.0" + version_number: "1.125.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0193e4f799..f239026c0a 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.125.0 +- API version: 1.125.1 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 58e94d8c81..140ec7291d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.0+176 +version: 1.125.1+177 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5d18987ba6..7ce4e0e300 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.0", + "version": "1.125.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index ee81998a8f..45410e78a0 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.125.0", + "version": "1.125.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a8625ec4e6..5d94e0e70d 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.0", + "version": "1.125.1", "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 60aec025a4..7a4c0cd7ee 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.125.0 + * 1.125.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 31fb98e2d1..248792918d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.0", + "version": "1.125.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 3068df625a..7f93d4e503 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.0", + "version": "1.125.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 5714c183d4..34366ca368 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.0", + "version": "1.125.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index afa5f6e3fd..1402c0b868 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.0", + "version": "1.125.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 071b2714847891c0cb35d95d79b4f432f28acaca Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:08:20 -0500 Subject: [PATCH 07/12] fix(server): `getTimeBuckets` not handling boolean filters correctly (#15567) fix boolean handling --- server/src/repositories/asset.repository.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index a53b1dbd92..00fa130bb4 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -605,10 +605,10 @@ export class AssetRepository implements IAssetRepository { .where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])), ) .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(!!options.isArchived, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) - .$if(!!options.isFavorite, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) + .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) + .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) - .$if(!!options.isDuplicate, (qb) => + .$if(options.isDuplicate !== undefined, (qb) => qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), ) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)), @@ -622,7 +622,7 @@ export class AssetRepository implements IAssetRepository { */ .select((eb) => eb.fn.countAll().as('count')) .groupBy('timeBucket') - .orderBy('timeBucket', 'desc') + .orderBy('timeBucket', options.order ?? 'desc') .execute() as any as Promise ); } @@ -666,7 +666,7 @@ export class AssetRepository implements IAssetRepository { .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) .where('assets.isVisible', '=', true) .where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, '')) - .orderBy('assets.localDateTime', 'desc') + .orderBy('assets.localDateTime', options.order ?? 'desc') .execute() as any as Promise; } From a1691ddc0f252aec503627b7bb619c959854da7c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 23 Jan 2025 16:38:34 -0500 Subject: [PATCH 08/12] fix(web): auth page padding (#15569) --- .../components/layouts/AuthPageLayout.svelte | 8 +- .../routes/auth/change-password/+page.svelte | 28 +++---- web/src/routes/auth/login/+page.svelte | 84 +++++++++---------- web/src/routes/auth/register/+page.svelte | 10 +-- 4 files changed, 60 insertions(+), 70 deletions(-) diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index c6ccec0f51..24e28078c1 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -19,10 +19,8 @@ -
- - {@render children?.()} - -
+ + {@render children?.()} + diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index a99e70bacb..75e9f9927c 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -34,29 +34,25 @@ -
- +
+ {$t('hi_user', { values: { name: $user.name, email: $user.email } })} {$t('change_password_description')} -
- - - - - + + + - - - {errorMessage} - + + + {errorMessage} + -
- -
-
+
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index bc062292fc..e60cd5f145 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -6,7 +6,7 @@ import { oauth } from '$lib/utils'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; import { login } from '@immich/sdk'; - import { Alert, Button, Field, Input, PasswordInput } from '@immich/ui'; + import { Alert, Button, Field, Input, PasswordInput, Stack } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -101,43 +101,43 @@ {#if $featureFlags.loaded} - {#if $serverConfig.loginPageMessage} - - - {@html $serverConfig.loginPageMessage} - - {/if} - - {#if !oauthLoading && $featureFlags.passwordLogin} -
- {#if errorMessage} - - {/if} - - - - - - - - - - - - {/if} - - {#if $featureFlags.oauth} - {#if $featureFlags.passwordLogin} -
-
- - {$t('or').toUpperCase()} - -
+ + {#if $serverConfig.loginPageMessage} + + + {@html $serverConfig.loginPageMessage} + {/if} -
+ + {#if !oauthLoading && $featureFlags.passwordLogin} +
+ {#if errorMessage} + + {/if} + + + + + + + + + + + + {/if} + + {#if $featureFlags.oauth} + {#if $featureFlags.passwordLogin} +
+
+ + {$t('or').toUpperCase()} + +
+ {/if} {#if oauthError} {/if} @@ -152,11 +152,11 @@ > {$serverConfig.oauthButtonText} -
- {/if} + {/if} - {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} - - {/if} + {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} + + {/if} +
{/if} diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index 50551358ea..f3bc494d95 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -47,13 +47,11 @@ -
- +
+ {$t('admin.registration_description')} -
- @@ -74,8 +72,6 @@ {/if} -
- -
+
From 995314446b64b2e140ff13b4c517bb10cbd9eb55 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jan 2025 16:23:23 -0600 Subject: [PATCH 09/12] feat(web): neon light behinds login form (#15570) --- .../components/layouts/AuthPageLayout.svelte | 7 ++++- web/static/immich-logo-no-bg.svg | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 web/static/immich-logo-no-bg.svg diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 24e28078c1..f4532902c2 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -10,7 +10,12 @@ let { title, children }: Props = $props(); -
+
+
+ Immich logo +
+
+ diff --git a/web/static/immich-logo-no-bg.svg b/web/static/immich-logo-no-bg.svg new file mode 100644 index 0000000000..376fa6f3e8 --- /dev/null +++ b/web/static/immich-logo-no-bg.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + From 1869b1b41a2a930f002d50057e26e5dbb5af5573 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 23 Jan 2025 18:10:17 -0500 Subject: [PATCH 10/12] refactor: repositories (#15561) * refactor: version history repository * refactor: oauth repository * refactor: trash repository * refactor: telemetry repository * refactor: metadata repository * refactor: cron repository * refactor: map repository * refactor: server-info repository * refactor: album user repository * refactor: notification repository --- server/src/app.module.ts | 5 +- .../controllers/notification.controller.ts | 2 +- server/src/emails/album-invite.email.tsx | 2 +- server/src/emails/album-update.email.tsx | 2 +- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 2 +- server/src/interfaces/album-user.interface.ts | 18 --- server/src/interfaces/cron.interface.ts | 20 ---- server/src/interfaces/job.interface.ts | 2 +- server/src/interfaces/map.interface.ts | 31 ------ server/src/interfaces/metadata.interface.ts | 71 ------------ .../src/interfaces/notification.interface.ts | 101 ----------------- server/src/interfaces/oauth.interface.ts | 22 ---- .../src/interfaces/server-info.interface.ts | 24 ---- server/src/interfaces/telemetry.interface.ts | 23 ---- server/src/interfaces/trash.interface.ts | 8 -- .../interfaces/version-history.interface.ts | 9 -- .../src/repositories/album-user.repository.ts | 13 ++- server/src/repositories/asset.repository.ts | 2 +- server/src/repositories/cron.repository.ts | 17 ++- server/src/repositories/index.ts | 30 ++--- server/src/repositories/map.repository.ts | 33 ++++-- .../src/repositories/metadata.repository.ts | 67 ++++++++++- .../notification.repository.spec.ts | 3 +- .../repositories/notification.repository.ts | 104 ++++++++++++++++-- server/src/repositories/oauth.repository.ts | 17 ++- .../repositories/server-info.repository.ts | 21 +++- .../src/repositories/telemetry.repository.ts | 7 +- server/src/repositories/trash.repository.ts | 3 +- .../version-history.repository.ts | 10 +- server/src/services/album.service.spec.ts | 2 +- server/src/services/auth.service.spec.ts | 3 +- server/src/services/auth.service.ts | 2 +- server/src/services/backup.service.spec.ts | 3 +- server/src/services/base.service.ts | 40 +++---- server/src/services/job.service.spec.ts | 4 +- server/src/services/library.service.spec.ts | 3 +- server/src/services/map.service.spec.ts | 2 +- server/src/services/metadata.service.spec.ts | 5 +- server/src/services/metadata.service.ts | 4 +- .../src/services/notification.service.spec.ts | 3 +- server/src/services/notification.service.ts | 2 +- server/src/services/trash.service.spec.ts | 2 +- server/src/services/version.service.spec.ts | 4 +- server/src/types.ts | 21 ++++ server/test/fixtures/metadata.stub.ts | 2 +- .../album-user.repository.mock.ts | 2 +- .../test/repositories/cron.repository.mock.ts | 2 +- .../test/repositories/map.repository.mock.ts | 2 +- .../repositories/metadata.repository.mock.ts | 2 +- .../notification.repository.mock.ts | 2 +- .../repositories/oauth.repository.mock.ts | 2 +- .../server-info.repository.mock.ts | 2 +- .../repositories/telemetry.repository.mock.ts | 8 +- .../repositories/trash.repository.mock.ts | 2 +- .../version-history.repository.mock.ts | 2 +- server/test/utils.ts | 42 +++++-- 57 files changed, 372 insertions(+), 469 deletions(-) delete mode 100644 server/src/interfaces/album-user.interface.ts delete mode 100644 server/src/interfaces/cron.interface.ts delete mode 100644 server/src/interfaces/map.interface.ts delete mode 100644 server/src/interfaces/metadata.interface.ts delete mode 100644 server/src/interfaces/notification.interface.ts delete mode 100644 server/src/interfaces/oauth.interface.ts delete mode 100644 server/src/interfaces/server-info.interface.ts delete mode 100644 server/src/interfaces/telemetry.interface.ts delete mode 100644 server/src/interfaces/trash.interface.ts delete mode 100644 server/src/interfaces/version-history.interface.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index d088af6188..cd19972206 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -13,7 +13,6 @@ import { entities } from 'src/entities'; import { ImmichWorker } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository } from 'src/interfaces/job.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; @@ -22,7 +21,7 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { providers, repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { teardownTelemetry } from 'src/repositories/telemetry.repository'; +import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; @@ -67,7 +66,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { logger: LoggingRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository, + private telemetryRepository: TelemetryRepository, ) { logger.setAppName(this.worker); } diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index 27034fd63a..39946a9fc9 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -3,8 +3,8 @@ import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; -import { EmailTemplate } from 'src/interfaces/notification.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { EmailTemplate } from 'src/repositories/notification.repository'; import { NotificationService } from 'src/services/notification.service'; @ApiTags('Notifications') diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 0b3819b332..4bd7abc305 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface'; +import { AlbumInviteEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumInviteEmail = ({ diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 9dcd858e93..2311e896e1 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface'; +import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumUpdateEmail = ({ diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index 8ba19436c6..ac9bdbe0ea 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,7 +1,7 @@ import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { TestEmailProps } from 'src/interfaces/notification.interface'; +import { TestEmailProps } from 'src/repositories/notification.repository'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index ced0b77698..11a6602711 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { WelcomeEmailProps } from 'src/interfaces/notification.interface'; +import { WelcomeEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { diff --git a/server/src/interfaces/album-user.interface.ts b/server/src/interfaces/album-user.interface.ts deleted file mode 100644 index 835e835589..0000000000 --- a/server/src/interfaces/album-user.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Insertable, Selectable, Updateable } from 'kysely'; -import { AlbumsSharedUsersUsers } from 'src/db'; - -export const IAlbumUserRepository = 'IAlbumUserRepository'; - -export type AlbumPermissionId = { - albumsId: string; - usersId: string; -}; - -export interface IAlbumUserRepository { - create(albumUser: Insertable): Promise>; - update( - id: AlbumPermissionId, - albumPermission: Updateable, - ): Promise>; - delete(id: AlbumPermissionId): Promise; -} diff --git a/server/src/interfaces/cron.interface.ts b/server/src/interfaces/cron.interface.ts deleted file mode 100644 index ceb554864a..0000000000 --- a/server/src/interfaces/cron.interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const ICronRepository = 'ICronRepository'; - -type CronBase = { - name: string; - start?: boolean; -}; - -export type CronCreate = CronBase & { - expression: string; - onTick: () => void; -}; - -export type CronUpdate = CronBase & { - expression?: string; -}; - -export interface ICronRepository { - create(cron: CronCreate): void; - update(cron: CronUpdate): void; -} diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 7976f81302..1f2b92074a 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -1,5 +1,5 @@ import { ClassConstructor } from 'class-transformer'; -import { EmailImageAttachment } from 'src/interfaces/notification.interface'; +import { EmailImageAttachment } from 'src/repositories/notification.repository'; export enum QueueName { THUMBNAIL_GENERATION = 'thumbnailGeneration', diff --git a/server/src/interfaces/map.interface.ts b/server/src/interfaces/map.interface.ts deleted file mode 100644 index 0a04840a96..0000000000 --- a/server/src/interfaces/map.interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const IMapRepository = 'IMapRepository'; - -export interface MapMarkerSearchOptions { - isArchived?: boolean; - isFavorite?: boolean; - fileCreatedBefore?: Date; - fileCreatedAfter?: Date; -} - -export interface GeoPoint { - latitude: number; - longitude: number; -} - -export interface ReverseGeocodeResult { - country: string | null; - state: string | null; - city: string | null; -} - -export interface MapMarker extends ReverseGeocodeResult { - id: string; - lat: number; - lon: number; -} - -export interface IMapRepository { - init(): Promise; - reverseGeocode(point: GeoPoint): Promise; - getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise; -} diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts deleted file mode 100644 index 574420e27a..0000000000 --- a/server/src/interfaces/metadata.interface.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { BinaryField, Tags } from 'exiftool-vendored'; - -export const IMetadataRepository = 'IMetadataRepository'; - -export interface ExifDuration { - Value: number; - Scale?: number; -} - -type StringOrNumber = string | number; - -type TagsWithWrongTypes = - | 'FocalLength' - | 'Duration' - | 'Description' - | 'ImageDescription' - | 'RegionInfo' - | 'TagsList' - | 'Keywords' - | 'HierarchicalSubject' - | 'ISO'; -export interface ImmichTags extends Omit { - ContentIdentifier?: string; - MotionPhoto?: number; - MotionPhotoVersion?: number; - MotionPhotoPresentationTimestampUs?: number; - MediaGroupUUID?: string; - ImagePixelDepth?: string; - FocalLength?: number; - Duration?: number | string | ExifDuration; - EmbeddedVideoType?: string; - EmbeddedVideoFile?: BinaryField; - MotionPhotoVideo?: BinaryField; - TagsList?: StringOrNumber[]; - HierarchicalSubject?: StringOrNumber[]; - Keywords?: StringOrNumber | StringOrNumber[]; - ISO?: number | number[]; - - // Type is wrong, can also be number. - Description?: StringOrNumber; - ImageDescription?: StringOrNumber; - - // Extended properties for image regions, such as faces - RegionInfo?: { - AppliedToDimensions: { - W: number; - H: number; - Unit: string; - }; - RegionList: { - Area: { - // (X,Y) // center of the rectangle - X: number; - Y: number; - W: number; - H: number; - Unit: string; - }; - Rotation?: number; - Type?: string; - Name?: string; - }[]; - }; -} - -export interface IMetadataRepository { - teardown(): Promise; - readTags(path: string): Promise; - writeTags(path: string, tags: Partial): Promise; - extractBinaryTag(tagName: string, path: string): Promise; -} diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts deleted file mode 100644 index b20b3c50ae..0000000000 --- a/server/src/interfaces/notification.interface.ts +++ /dev/null @@ -1,101 +0,0 @@ -export const INotificationRepository = 'INotificationRepository'; - -export type EmailImageAttachment = { - filename: string; - path: string; - cid: string; -}; - -export type SendEmailOptions = { - from: string; - to: string; - replyTo?: string; - subject: string; - html: string; - text: string; - imageAttachments?: EmailImageAttachment[]; - smtp: SmtpOptions; -}; - -export type SmtpOptions = { - host: string; - port?: number; - username?: string; - password?: string; - ignoreCert?: boolean; -}; - -export enum EmailTemplate { - TEST_EMAIL = 'test', - - // AUTH - WELCOME = 'welcome', - RESET_PASSWORD = 'reset-password', - - // ALBUM - ALBUM_INVITE = 'album-invite', - ALBUM_UPDATE = 'album-update', -} - -interface BaseEmailProps { - baseUrl: string; - customTemplate?: string; -} - -export interface TestEmailProps extends BaseEmailProps { - displayName: string; -} - -export interface WelcomeEmailProps extends BaseEmailProps { - displayName: string; - username: string; - password?: string; -} - -export interface AlbumInviteEmailProps extends BaseEmailProps { - albumName: string; - albumId: string; - senderName: string; - recipientName: string; - cid?: string; -} - -export interface AlbumUpdateEmailProps extends BaseEmailProps { - albumName: string; - albumId: string; - recipientName: string; - cid?: string; -} - -export type EmailRenderRequest = - | { - template: EmailTemplate.TEST_EMAIL; - data: TestEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.WELCOME; - data: WelcomeEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.ALBUM_INVITE; - data: AlbumInviteEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.ALBUM_UPDATE; - data: AlbumUpdateEmailProps; - customTemplate: string; - }; - -export type SendEmailResponse = { - messageId: string; - response: any; -}; - -export interface INotificationRepository { - renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>; - sendEmail(options: SendEmailOptions): Promise; - verifySmtp(options: SmtpOptions): Promise; -} diff --git a/server/src/interfaces/oauth.interface.ts b/server/src/interfaces/oauth.interface.ts deleted file mode 100644 index 5e629726a0..0000000000 --- a/server/src/interfaces/oauth.interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UserinfoResponse } from 'openid-client'; - -export const IOAuthRepository = 'IOAuthRepository'; - -export type OAuthConfig = { - clientId: string; - clientSecret: string; - issuerUrl: string; - mobileOverrideEnabled: boolean; - mobileRedirectUri: string; - profileSigningAlgorithm: string; - scope: string; - signingAlgorithm: string; -}; -export type OAuthProfile = UserinfoResponse; - -export interface IOAuthRepository { - init(): void; - authorize(config: OAuthConfig, redirectUrl: string): Promise; - getLogoutEndpoint(config: OAuthConfig): Promise; - getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise; -} diff --git a/server/src/interfaces/server-info.interface.ts b/server/src/interfaces/server-info.interface.ts deleted file mode 100644 index 6dc857ddea..0000000000 --- a/server/src/interfaces/server-info.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface GitHubRelease { - id: number; - url: string; - tag_name: string; - name: string; - created_at: string; - published_at: string; - body: string; -} - -export interface ServerBuildVersions { - nodejs: string; - ffmpeg: string; - libvips: string; - exiftool: string; - imagemagick: string; -} - -export const IServerInfoRepository = 'IServerInfoRepository'; - -export interface IServerInfoRepository { - getGitHubRelease(): Promise; - getBuildVersions(): Promise; -} diff --git a/server/src/interfaces/telemetry.interface.ts b/server/src/interfaces/telemetry.interface.ts deleted file mode 100644 index 688e52c21e..0000000000 --- a/server/src/interfaces/telemetry.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MetricOptions } from '@opentelemetry/api'; -import { ClassConstructor } from 'class-transformer'; - -export const ITelemetryRepository = 'ITelemetryRepository'; - -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 ITelemetryRepository { - setup(options: { repositories: ClassConstructor[] }): void; - api: IMetricGroupRepository; - host: IMetricGroupRepository; - jobs: IMetricGroupRepository; - repo: IMetricGroupRepository; -} diff --git a/server/src/interfaces/trash.interface.ts b/server/src/interfaces/trash.interface.ts deleted file mode 100644 index 38e7c523ce..0000000000 --- a/server/src/interfaces/trash.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const ITrashRepository = 'ITrashRepository'; - -export interface ITrashRepository { - empty(userId: string): Promise; - restore(userId: string): Promise; - restoreAll(assetIds: string[]): Promise; - getDeletedIds(): AsyncIterableIterator<{ id: string }>; -} diff --git a/server/src/interfaces/version-history.interface.ts b/server/src/interfaces/version-history.interface.ts deleted file mode 100644 index c38552c24f..0000000000 --- a/server/src/interfaces/version-history.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { VersionHistoryEntity } from 'src/entities/version-history.entity'; - -export const IVersionHistoryRepository = 'IVersionHistoryRepository'; - -export interface IVersionHistoryRepository { - create(version: Omit): Promise; - getAll(): Promise; - getLatest(): Promise; -} diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 5895721b63..f363f2e91a 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -4,10 +4,14 @@ import { InjectKysely } from 'nestjs-kysely'; import { AlbumsSharedUsersUsers, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; -import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface'; + +export type AlbumPermissionId = { + albumsId: string; + usersId: string; +}; @Injectable() -export class AlbumUserRepository implements IAlbumUserRepository { +export class AlbumUserRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) @@ -16,10 +20,7 @@ export class AlbumUserRepository implements IAlbumUserRepository { } @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) - update( - { usersId, albumsId }: AlbumPermissionId, - dto: Updateable, - ): Promise> { + update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable) { return this.db .updateTable('albums_shared_users_users') .set(dto) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 00fa130bb4..9c506197d6 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -43,8 +43,8 @@ import { WithProperty, WithoutProperty, } from 'src/interfaces/asset.interface'; -import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface'; import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface'; +import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository'; import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database'; import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination'; diff --git a/server/src/repositories/cron.repository.ts b/server/src/repositories/cron.repository.ts index 12533f67af..e6e8fe7568 100644 --- a/server/src/repositories/cron.repository.ts +++ b/server/src/repositories/cron.repository.ts @@ -1,11 +1,24 @@ import { Injectable } from '@nestjs/common'; import { SchedulerRegistry } from '@nestjs/schedule'; import { CronJob, CronTime } from 'cron'; -import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +type CronBase = { + name: string; + start?: boolean; +}; + +export type CronCreate = CronBase & { + expression: string; + onTick: () => void; +}; + +export type CronUpdate = CronBase & { + expression?: string; +}; + @Injectable() -export class CronRepository implements ICronRepository { +export class CronRepository { constructor( private schedulerRegistry: SchedulerRegistry, private logger: LoggingRepository, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7f4619d3de..b54c69e117 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -1,33 +1,23 @@ -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.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 { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; -import { INotificationRepository } from 'src/interfaces/notification.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -71,44 +61,44 @@ import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ AccessRepository, ActivityRepository, + AlbumUserRepository, AuditRepository, ApiKeyRepository, ConfigRepository, + CronRepository, LoggingRepository, + MapRepository, MediaRepository, MemoryRepository, + MetadataRepository, + NotificationRepository, + OAuthRepository, + ServerInfoRepository, + TelemetryRepository, + TrashRepository, ViewRepository, + VersionHistoryRepository, ]; export const providers = [ { provide: IAlbumRepository, useClass: AlbumRepository }, - { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, - { provide: ICronRepository, useClass: CronRepository }, { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IDatabaseRepository, useClass: DatabaseRepository }, { provide: IEventRepository, useClass: EventRepository }, { provide: IJobRepository, useClass: JobRepository }, { provide: ILibraryRepository, useClass: LibraryRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, - { provide: IMapRepository, useClass: MapRepository }, - { provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMoveRepository, useClass: MoveRepository }, - { provide: INotificationRepository, useClass: NotificationRepository }, - { provide: IOAuthRepository, useClass: OAuthRepository }, { provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPersonRepository, useClass: PersonRepository }, { provide: IProcessRepository, useClass: ProcessRepository }, { provide: ISearchRepository, useClass: SearchRepository }, - { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISessionRepository, useClass: SessionRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: IStackRepository, useClass: StackRepository }, { provide: IStorageRepository, useClass: StorageRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, - { provide: ITelemetryRepository, useClass: TelemetryRepository }, - { provide: ITrashRepository, useClass: TrashRepository }, { provide: IUserRepository, useClass: UserRepository }, - { provide: IVersionHistoryRepository, useClass: VersionHistoryRepository }, ]; diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 3c5c3f5671..af24b0c94e 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -11,24 +11,41 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; import { AssetEntity, withExif } from 'src/entities/asset.entity'; import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; import { LogLevel, SystemMetadataKey } from 'src/enum'; -import { - GeoPoint, - IMapRepository, - MapMarker, - MapMarkerSearchOptions, - ReverseGeocodeResult, -} from 'src/interfaces/map.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export interface MapMarkerSearchOptions { + isArchived?: boolean; + isFavorite?: boolean; + fileCreatedBefore?: Date; + fileCreatedAfter?: Date; +} + +export interface GeoPoint { + latitude: number; + longitude: number; +} + +export interface ReverseGeocodeResult { + country: string | null; + state: string | null; + city: string | null; +} + +export interface MapMarker extends ReverseGeocodeResult { + id: string; + lat: number; + lon: number; +} + interface MapDB extends DB { geodata_places_tmp: GeodataPlaces; naturalearth_countries_tmp: NaturalearthCountries; } @Injectable() -export class MapRepository implements IMapRepository { +export class MapRepository { constructor( private configRepository: ConfigRepository, @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository, diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 90a3a9e765..3f297d709b 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -1,11 +1,72 @@ import { Injectable } from '@nestjs/common'; -import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored'; +import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored'; import geotz from 'geo-tz'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +interface ExifDuration { + Value: number; + Scale?: number; +} + +type StringOrNumber = string | number; + +type TagsWithWrongTypes = + | 'FocalLength' + | 'Duration' + | 'Description' + | 'ImageDescription' + | 'RegionInfo' + | 'TagsList' + | 'Keywords' + | 'HierarchicalSubject' + | 'ISO'; + +export interface ImmichTags extends Omit { + ContentIdentifier?: string; + MotionPhoto?: number; + MotionPhotoVersion?: number; + MotionPhotoPresentationTimestampUs?: number; + MediaGroupUUID?: string; + ImagePixelDepth?: string; + FocalLength?: number; + Duration?: number | string | ExifDuration; + EmbeddedVideoType?: string; + EmbeddedVideoFile?: BinaryField; + MotionPhotoVideo?: BinaryField; + TagsList?: StringOrNumber[]; + HierarchicalSubject?: StringOrNumber[]; + Keywords?: StringOrNumber | StringOrNumber[]; + ISO?: number | number[]; + + // Type is wrong, can also be number. + Description?: StringOrNumber; + ImageDescription?: StringOrNumber; + + // Extended properties for image regions, such as faces + RegionInfo?: { + AppliedToDimensions: { + W: number; + H: number; + Unit: string; + }; + RegionList: { + Area: { + // (X,Y) // center of the rectangle + X: number; + Y: number; + W: number; + H: number; + Unit: string; + }; + Rotation?: number; + Type?: string; + Name?: string; + }[]; + }; +} + @Injectable() -export class MetadataRepository implements IMetadataRepository { +export class MetadataRepository { private exiftool = new ExifTool({ defaultVideosToUTC: true, backfillTimezones: true, diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index f952e9ebed..0d8e826c66 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -1,6 +1,5 @@ -import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; +import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; import { ILoggingRepository } from 'src/types'; import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { Mocked } from 'vitest'; diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index ecdaed3866..fdb74cfdb2 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -6,18 +6,104 @@ import { AlbumInviteEmail } from 'src/emails/album-invite.email'; import { AlbumUpdateEmail } from 'src/emails/album-update.email'; import { TestEmail } from 'src/emails/test.email'; import { WelcomeEmail } from 'src/emails/welcome.email'; -import { - EmailRenderRequest, - EmailTemplate, - INotificationRepository, - SendEmailOptions, - SendEmailResponse, - SmtpOptions, -} from 'src/interfaces/notification.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export type EmailImageAttachment = { + filename: string; + path: string; + cid: string; +}; + +export type SendEmailOptions = { + from: string; + to: string; + replyTo?: string; + subject: string; + html: string; + text: string; + imageAttachments?: EmailImageAttachment[]; + smtp: SmtpOptions; +}; + +export type SmtpOptions = { + host: string; + port?: number; + username?: string; + password?: string; + ignoreCert?: boolean; +}; + +export enum EmailTemplate { + TEST_EMAIL = 'test', + + // AUTH + WELCOME = 'welcome', + RESET_PASSWORD = 'reset-password', + + // ALBUM + ALBUM_INVITE = 'album-invite', + ALBUM_UPDATE = 'album-update', +} + +interface BaseEmailProps { + baseUrl: string; + customTemplate?: string; +} + +export interface TestEmailProps extends BaseEmailProps { + displayName: string; +} + +export interface WelcomeEmailProps extends BaseEmailProps { + displayName: string; + username: string; + password?: string; +} + +export interface AlbumInviteEmailProps extends BaseEmailProps { + albumName: string; + albumId: string; + senderName: string; + recipientName: string; + cid?: string; +} + +export interface AlbumUpdateEmailProps extends BaseEmailProps { + albumName: string; + albumId: string; + recipientName: string; + cid?: string; +} + +export type EmailRenderRequest = + | { + template: EmailTemplate.TEST_EMAIL; + data: TestEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.WELCOME; + data: WelcomeEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.ALBUM_INVITE; + data: AlbumInviteEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.ALBUM_UPDATE; + data: AlbumUpdateEmailProps; + customTemplate: string; + }; + +export type SendEmailResponse = { + messageId: string; + response: any; +}; + @Injectable() -export class NotificationRepository implements INotificationRepository { +export class NotificationRepository { constructor(private logger: LoggingRepository) { this.logger.setContext(NotificationRepository.name); } diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index dfd36edc2a..85263cd647 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -1,10 +1,21 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { custom, generators, Issuer } from 'openid-client'; -import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface'; +import { custom, generators, Issuer, UserinfoResponse } from 'openid-client'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export type OAuthConfig = { + clientId: string; + clientSecret: string; + issuerUrl: string; + mobileOverrideEnabled: boolean; + mobileRedirectUri: string; + profileSigningAlgorithm: string; + scope: string; + signingAlgorithm: string; +}; +export type OAuthProfile = UserinfoResponse; + @Injectable() -export class OAuthRepository implements IOAuthRepository { +export class OAuthRepository { constructor(private logger: LoggingRepository) { this.logger.setContext(OAuthRepository.name); } diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index c0c0f88723..deb24123d0 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,10 +4,27 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; -import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export interface GitHubRelease { + id: number; + url: string; + tag_name: string; + name: string; + created_at: string; + published_at: string; + body: string; +} + +export interface ServerBuildVersions { + nodejs: string; + ffmpeg: string; + libvips: string; + exiftool: string; + imagemagick: string; +} + const exec = promisify(execCallback); const maybeFirstLine = async (command: string): Promise => { try { @@ -34,7 +51,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => { }; @Injectable() -export class ServerInfoRepository implements IServerInfoRepository { +export class ServerInfoRepository { constructor( private configRepository: ConfigRepository, private logger: LoggingRepository, diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 7a82ba07e7..7f93d4deba 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -15,11 +15,12 @@ import { MetricService } from 'nestjs-otel'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; import { serverVersion } from 'src/constants'; import { ImmichTelemetry, MetadataKey } from 'src/enum'; -import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -class MetricGroupRepository implements IMetricGroupRepository { +type MetricGroupOptions = { enabled: boolean }; + +export class MetricGroupRepository { private enabled = false; constructor(private metricService: MetricService) {} @@ -86,7 +87,7 @@ export const teardownTelemetry = async () => { }; @Injectable() -export class TelemetryRepository implements ITelemetryRepository { +export class TelemetryRepository { api: MetricGroupRepository; host: MetricGroupRepository; jobs: MetricGroupRepository; diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts index 06e75a8d2e..69507b1d58 100644 --- a/server/src/repositories/trash.repository.ts +++ b/server/src/repositories/trash.repository.ts @@ -3,9 +3,8 @@ import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetStatus } from 'src/enum'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; -export class TrashRepository implements ITrashRepository { +export class TrashRepository { constructor(@InjectKysely() private db: Kysely) {} getDeletedIds(): AsyncIterableIterator<{ id: string }> { diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts index e6ec8edcf4..063ee0da84 100644 --- a/server/src/repositories/version-history.repository.ts +++ b/server/src/repositories/version-history.repository.ts @@ -3,25 +3,23 @@ import { Insertable, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB, VersionHistory } from 'src/db'; import { GenerateSql } from 'src/decorators'; -import { VersionHistoryEntity } from 'src/entities/version-history.entity'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; @Injectable() -export class VersionHistoryRepository implements IVersionHistoryRepository { +export class VersionHistoryRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql() - getAll(): Promise { + getAll() { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute(); } @GenerateSql() - getLatest(): Promise { + getLatest() { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); } @GenerateSql({ params: [{ version: 'v1.123.0' }] }) - create(version: Insertable): Promise { + create(version: Insertable) { return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow(); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 99c794adc9..fe732843b6 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -2,11 +2,11 @@ import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AlbumUserRole } from 'src/enum'; -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AlbumService } from 'src/services/album.service'; +import { IAlbumUserRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index ffa280677a..780d802922 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -5,13 +5,12 @@ import { UserEntity } from 'src/entities/user.entity'; import { AuthType, Permission } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AuthService } from 'src/services/auth.service'; -import { IApiKeyRepository } from 'src/types'; +import { IApiKeyRepository, IOAuthRepository } from 'src/types'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { sessionStub } from 'test/fixtures/session.stub'; diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 4c0cdbab91..f46eb93111 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -19,7 +19,7 @@ import { import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum'; -import { OAuthProfile } from 'src/interfaces/oauth.interface'; +import { OAuthProfile } from 'src/repositories/oauth.repository'; import { BaseService } from 'src/services/base.service'; import { AuthApiKey } from 'src/types'; import { isGranted } from 'src/utils/access'; diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 29adf9d8e1..33d77a59aa 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -2,14 +2,13 @@ import { PassThrough } from 'node:stream'; import { defaults, SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { ImmichWorker, StorageFolder } from 'src/enum'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BackupService } from 'src/services/backup.service'; -import { IConfigRepository } from 'src/types'; +import { IConfigRepository, ICronRepository } from 'src/types'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { mockSpawn, newTestService } from 'test/utils'; import { describe, Mocked } from 'vitest'; diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 398b1508f5..865a16a9da 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -6,44 +6,44 @@ import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.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 { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; -import { INotificationRepository } from 'src/interfaces/notification.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -57,10 +57,10 @@ export class BaseService { protected activityRepository: ActivityRepository, protected auditRepository: AuditRepository, @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, - @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository, + protected albumUserRepository: AlbumUserRepository, @Inject(IAssetRepository) protected assetRepository: IAssetRepository, protected configRepository: ConfigRepository, - @Inject(ICronRepository) protected cronRepository: ICronRepository, + protected cronRepository: CronRepository, @Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository, @Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository, @Inject(IEventRepository) protected eventRepository: IEventRepository, @@ -68,28 +68,28 @@ export class BaseService { protected keyRepository: ApiKeyRepository, @Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository, @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository, - @Inject(IMapRepository) protected mapRepository: IMapRepository, + protected mapRepository: MapRepository, protected mediaRepository: MediaRepository, protected memoryRepository: MemoryRepository, - @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository, + protected metadataRepository: MetadataRepository, @Inject(IMoveRepository) protected moveRepository: IMoveRepository, - @Inject(INotificationRepository) protected notificationRepository: INotificationRepository, - @Inject(IOAuthRepository) protected oauthRepository: IOAuthRepository, + protected notificationRepository: NotificationRepository, + protected oauthRepository: OAuthRepository, @Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository, @Inject(IPersonRepository) protected personRepository: IPersonRepository, @Inject(IProcessRepository) protected processRepository: IProcessRepository, @Inject(ISearchRepository) protected searchRepository: ISearchRepository, - @Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository, + protected serverInfoRepository: ServerInfoRepository, @Inject(ISessionRepository) protected sessionRepository: ISessionRepository, @Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository, @Inject(IStackRepository) protected stackRepository: IStackRepository, @Inject(IStorageRepository) protected storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository, @Inject(ITagRepository) protected tagRepository: ITagRepository, - @Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository, - @Inject(ITrashRepository) protected trashRepository: ITrashRepository, + protected telemetryRepository: TelemetryRepository, + protected trashRepository: TrashRepository, @Inject(IUserRepository) protected userRepository: IUserRepository, - @Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository, + protected versionRepository: VersionHistoryRepository, protected viewRepository: ViewRepository, ) { this.logger.setContext(this.constructor.name); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 195ed10156..5d11f895a1 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -3,10 +3,10 @@ import { defaults, SystemConfig } from 'src/config'; import { ImmichWorker } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; import { JobService } from 'src/services/job.service'; import { IConfigRepository, ILoggingRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; +import { ITelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; @@ -16,7 +16,7 @@ describe(JobService.name, () => { let configMock: Mocked; let jobMock: Mocked; let loggerMock: Mocked; - let telemetryMock: Mocked; + let telemetryMock: ITelemetryRepositoryMock; beforeEach(() => { ({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {})); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index e2d805b865..5f81d92ec2 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -5,7 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AssetType, ImmichWorker } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, @@ -18,7 +17,7 @@ import { import { ILibraryRepository } from 'src/interfaces/library.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { LibraryService } from 'src/services/library.service'; -import { IConfigRepository } from 'src/types'; +import { IConfigRepository, ICronRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { libraryStub } from 'test/fixtures/library.stub'; diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index fde2ba7e0f..30505f7f5b 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,7 +1,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { MapService } from 'src/services/map.service'; +import { IMapRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 4688e8b119..8cc6e014d2 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -10,15 +10,14 @@ import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interfac import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { MetadataService } from 'src/services/metadata.service'; -import { IConfigRepository, IMediaRepository } from 'src/types'; +import { IConfigRepository, IMapRepository, IMediaRepository, IMetadataRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 406f80038c..d5b7e6e4e4 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -18,8 +18,8 @@ import { WithoutProperty } from 'src/interfaces/asset.interface'; import { DatabaseLock } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface'; -import { ReverseGeocodeResult } from 'src/interfaces/map.interface'; -import { ImmichTags } from 'src/interfaces/metadata.interface'; +import { ReverseGeocodeResult } from 'src/repositories/map.repository'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { BaseService } from 'src/services/base.service'; import { isFaceImportEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 76da12bbd6..671cae0774 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -8,10 +8,11 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { EmailTemplate } from 'src/repositories/notification.repository'; import { NotificationService } from 'src/services/notification.service'; +import { INotificationRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 37b265c6ae..85f72443d4 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -12,7 +12,7 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface'; +import { EmailImageAttachment, EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index 4d877c9dfa..8b93e899e7 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { TrashService } from 'src/services/trash.service'; +import { ITrashRepository } from 'src/types'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newTestService } from 'test/utils'; diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 022e1de613..406d3c1439 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -4,11 +4,9 @@ import { serverVersion } from 'src/constants'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { VersionService } from 'src/services/version.service'; -import { IConfigRepository, ILoggingRepository } from 'src/types'; +import { IConfigRepository, ILoggingRepository, IServerInfoRepository, IVersionHistoryRepository } from 'src/types'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; diff --git a/server/src/types.ts b/server/src/types.ts index 69d9d8e647..9928669136 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -2,12 +2,22 @@ import { UserEntity } from 'src/entities/user.entity'; import { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T; @@ -23,9 +33,11 @@ export type RepositoryInterface = Pick; export type IActivityRepository = RepositoryInterface; export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; +export type IAlbumUserRepository = RepositoryInterface; export type IApiKeyRepository = RepositoryInterface; export type IAuditRepository = RepositoryInterface; export type IConfigRepository = RepositoryInterface; +export type ICronRepository = RepositoryInterface; export type ILoggingRepository = Pick< LoggingRepository, | 'verbose' @@ -39,9 +51,18 @@ export type ILoggingRepository = Pick< | 'setContext' | 'setAppName' >; +export type IMapRepository = RepositoryInterface; export type IMediaRepository = RepositoryInterface; export type IMemoryRepository = RepositoryInterface; +export type IMetadataRepository = RepositoryInterface; +export type IMetricGroupRepository = RepositoryInterface; +export type INotificationRepository = RepositoryInterface; +export type IOAuthRepository = RepositoryInterface; +export type IServerInfoRepository = RepositoryInterface; +export type ITelemetryRepository = RepositoryInterface; +export type ITrashRepository = RepositoryInterface; export type IViewRepository = RepositoryInterface; +export type IVersionHistoryRepository = RepositoryInterface; export type ActivityItem = | Awaited> diff --git a/server/test/fixtures/metadata.stub.ts b/server/test/fixtures/metadata.stub.ts index 05535303e4..e60d8d0eac 100644 --- a/server/test/fixtures/metadata.stub.ts +++ b/server/test/fixtures/metadata.stub.ts @@ -1,4 +1,4 @@ -import { ImmichTags } from 'src/interfaces/metadata.interface'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { personStub } from 'test/fixtures/person.stub'; export const metadataStub = { diff --git a/server/test/repositories/album-user.repository.mock.ts b/server/test/repositories/album-user.repository.mock.ts index 70c0487256..aa9436e33d 100644 --- a/server/test/repositories/album-user.repository.mock.ts +++ b/server/test/repositories/album-user.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; +import { IAlbumUserRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newAlbumUserRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/cron.repository.mock.ts b/server/test/repositories/cron.repository.mock.ts index 2b0784e8ac..cc856909c8 100644 --- a/server/test/repositories/cron.repository.mock.ts +++ b/server/test/repositories/cron.repository.mock.ts @@ -1,4 +1,4 @@ -import { ICronRepository } from 'src/interfaces/cron.interface'; +import { ICronRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newCronRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/map.repository.mock.ts b/server/test/repositories/map.repository.mock.ts index 703e8696f1..4b56b9443a 100644 --- a/server/test/repositories/map.repository.mock.ts +++ b/server/test/repositories/map.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMapRepository } from 'src/interfaces/map.interface'; +import { IMapRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newMapRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 60c5644b36..e9bb68b95b 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IMetadataRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newMetadataRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/notification.repository.mock.ts b/server/test/repositories/notification.repository.mock.ts index 16862dc3d7..2065a0bf3e 100644 --- a/server/test/repositories/notification.repository.mock.ts +++ b/server/test/repositories/notification.repository.mock.ts @@ -1,4 +1,4 @@ -import { INotificationRepository } from 'src/interfaces/notification.interface'; +import { INotificationRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newNotificationRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/oauth.repository.mock.ts b/server/test/repositories/oauth.repository.mock.ts index f87b3781e9..8980bfb14f 100644 --- a/server/test/repositories/oauth.repository.mock.ts +++ b/server/test/repositories/oauth.repository.mock.ts @@ -1,4 +1,4 @@ -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; +import { IOAuthRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newOAuthRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/server-info.repository.mock.ts b/server/test/repositories/server-info.repository.mock.ts index f55933d3c6..5e9ecd1387 100644 --- a/server/test/repositories/server-info.repository.mock.ts +++ b/server/test/repositories/server-info.repository.mock.ts @@ -1,4 +1,4 @@ -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { IServerInfoRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newServerInfoRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/telemetry.repository.mock.ts b/server/test/repositories/telemetry.repository.mock.ts index 2d537e888a..afadcea0cf 100644 --- a/server/test/repositories/telemetry.repository.mock.ts +++ b/server/test/repositories/telemetry.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; +import { ITelemetryRepository, RepositoryInterface } from 'src/types'; import { Mocked, vitest } from 'vitest'; const newMetricGroupMock = () => { @@ -10,7 +10,11 @@ const newMetricGroupMock = () => { }; }; -export const newTelemetryRepositoryMock = (): Mocked => { +export type ITelemetryRepositoryMock = { + [K in keyof ITelemetryRepository]: Mocked>; +}; + +export const newTelemetryRepositoryMock = (): ITelemetryRepositoryMock => { return { setup: vitest.fn(), api: newMetricGroupMock(), diff --git a/server/test/repositories/trash.repository.mock.ts b/server/test/repositories/trash.repository.mock.ts index 472b315b01..f983afdce8 100644 --- a/server/test/repositories/trash.repository.mock.ts +++ b/server/test/repositories/trash.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { ITrashRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newTrashRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/version-history.repository.mock.ts b/server/test/repositories/version-history.repository.mock.ts index 7c35e316d3..9ff7708796 100644 --- a/server/test/repositories/version-history.repository.mock.ts +++ b/server/test/repositories/version-history.repository.mock.ts @@ -1,4 +1,4 @@ -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; +import { IVersionHistoryRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newVersionHistoryRepositoryMock = (): Mocked => { diff --git a/server/test/utils.ts b/server/test/utils.ts index 0ab1739e14..94377ca18c 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -2,24 +2,42 @@ import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import { ImmichWorker } from 'src/enum'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { BaseService } from 'src/services/base.service'; import { IAccessRepository, IActivityRepository, + IAlbumUserRepository, IApiKeyRepository, IAuditRepository, + ICronRepository, ILoggingRepository, + IMapRepository, IMediaRepository, IMemoryRepository, + IMetadataRepository, + INotificationRepository, + IOAuthRepository, + IServerInfoRepository, + ITrashRepository, + IVersionHistoryRepository, IViewRepository, } from 'src/types'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; @@ -66,7 +84,7 @@ import { Mocked, vitest } from 'vitest'; type Overrides = { worker?: ImmichWorker; - metadataRepository?: IMetadataRepository; + metadataRepository?: MetadataRepository; }; type BaseServiceArgs = ConstructorParameters; type Constructor> = { @@ -125,10 +143,10 @@ export const newTestService = ( activityMock as IActivityRepository as ActivityRepository, auditMock as IAuditRepository as AuditRepository, albumMock, - albumUserMock, + albumUserMock as IAlbumUserRepository as AlbumUserRepository, assetMock, configMock, - cronMock, + cronMock as ICronRepository as CronRepository, cryptoMock, databaseMock, eventMock, @@ -136,28 +154,28 @@ export const newTestService = ( keyMock as IApiKeyRepository as ApiKeyRepository, libraryMock, machineLearningMock, - mapMock, + mapMock as IMapRepository as MapRepository, mediaMock as IMediaRepository as MediaRepository, memoryMock as IMemoryRepository as MemoryRepository, - metadataMock, + metadataMock as IMetadataRepository as MetadataRepository, moveMock, - notificationMock, - oauthMock, + notificationMock as INotificationRepository as NotificationRepository, + oauthMock as IOAuthRepository as OAuthRepository, partnerMock, personMock, processMock, searchMock, - serverInfoMock, + serverInfoMock as IServerInfoRepository as ServerInfoRepository, sessionMock, sharedLinkMock, stackMock, storageMock, systemMock, tagMock, - telemetryMock, - trashMock, + telemetryMock as unknown as TelemetryRepository, + trashMock as ITrashRepository as TrashRepository, userMock, - versionHistoryMock, + versionHistoryMock as IVersionHistoryRepository as VersionHistoryRepository, viewMock as IViewRepository as ViewRepository, ); From a07ae9b5b2dd8e747d1f620fdf5f5635277d5ee7 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:24:29 -0500 Subject: [PATCH 11/12] fix(server): set `updatedAt` on updates (#15573) * `updatedAt` triggers * drop function at the end --- .../1737672307560-AddUpdatedAtTriggers.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts diff --git a/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts b/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts new file mode 100644 index 0000000000..74dde826fb --- /dev/null +++ b/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts @@ -0,0 +1,102 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUpdatedAtTriggers1737672307560 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + create function updated_at() + returns trigger as $$ + begin + new."updatedAt" = now(); + return new; + end; + $$ language 'plpgsql'`); + + await queryRunner.query(` + create trigger activity_updated_at + before update on activity + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger albums_updated_at + before update on albums + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger api_keys_updated_at + before update on api_keys + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger asset_files_updated_at + before update on asset_files + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger assets_updated_at + before update on assets + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger libraries_updated_at + before update on libraries + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger memories_updated_at + before update on memories + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger partners_updated_at + before update on partners + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger person_updated_at + before update on person + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger sessions_updated_at + before update on sessions + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger tags_updated_at + before update on tags + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger users_updated_at + before update on users + for each row execute procedure updated_at() + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`drop trigger activity_updated_at on activity`); + await queryRunner.query(`drop trigger albums_updated_at on albums`); + await queryRunner.query(`drop trigger api_keys_updated_at on api_keys`); + await queryRunner.query(`drop trigger asset_files_updated_at on asset_files`); + await queryRunner.query(`drop trigger assets_updated_at on assets`); + await queryRunner.query(`drop trigger libraries_updated_at on libraries`); + await queryRunner.query(`drop trigger memories_updated_at on memories`); + await queryRunner.query(`drop trigger partners_updated_at on partners`); + await queryRunner.query(`drop trigger person_updated_at on person`); + await queryRunner.query(`drop trigger sessions_updated_at on sessions`); + await queryRunner.query(`drop trigger tags_updated_at on tags`); + await queryRunner.query(`drop trigger users_updated_at on users`); + await queryRunner.query(`drop function updated_at_trigger`); + } +} From 065d885ca0bb68e25c02aeb80e7536d81544926d Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 23 Jan 2025 21:33:24 -0500 Subject: [PATCH 12/12] fix(server): Fix for sorting faces during merging (#15571) * Fix for sorting faces * Put uneccessary orderBy in if statement --- server/src/repositories/person.repository.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index fdcecd9d0a..45183f39d6 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -133,10 +133,6 @@ export class PersonRepository implements IPersonRepository { ) .where('person.ownerId', '=', userId) .orderBy('person.isHidden', 'asc') - .orderBy(sql`NULLIF(person.name, '') is null`, 'asc') - .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc') - .orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`) - .orderBy('person.createdAt') .having((eb) => eb.or([ eb('person.name', '!=', ''), @@ -161,6 +157,13 @@ export class PersonRepository implements IPersonRepository { ), ), ) + .$if(!options?.closestFaceAssetId, (qb) => + qb + .orderBy(sql`NULLIF(person.name, '') is null`, 'asc') + .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc') + .orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`) + .orderBy('person.createdAt'), + ) .$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false)) .offset(pagination.skip ?? 0) .limit(pagination.take + 1)