From 97574d7296b195338c12b3f698d19ca30ecc6388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ventura?= Date: Tue, 4 Feb 2025 13:43:19 +0000 Subject: [PATCH 1/8] fix(web): prevent accidental modal closures on mouseup outside (#15900) --- web/src/lib/actions/click-outside.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index 1a421f1f5625e..92775546aae6d 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe } }; - document.addEventListener('click', handleClick, true); + document.addEventListener('mousedown', handleClick, true); node.addEventListener('keydown', handleKey, false); return { destroy() { - document.removeEventListener('click', handleClick, true); + document.removeEventListener('mousedown', handleClick, true); node.removeEventListener('keydown', handleKey, false); }, }; From 99de52479e6f12ed7c5886316dcd4ff5ad9936c1 Mon Sep 17 00:00:00 2001 From: Nicholas Flamy <30300649+NicholasFlamy@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:06:54 -0500 Subject: [PATCH 2/8] fix: pr template not being used and make some changes (#15893) fix-pr-template-and-make-some-changes-with-suggestions --- .github/PULL_REQUEST_TEMPLATE/config.yml | 1 - .../pull_request_template.md | 22 ------------ .github/pull_request_template.md | 36 +++++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/pull_request_template.md diff --git a/.github/PULL_REQUEST_TEMPLATE/config.yml b/.github/PULL_REQUEST_TEMPLATE/config.yml index 6663b04cbc63f..4172e3df95270 100644 --- a/.github/PULL_REQUEST_TEMPLATE/config.yml +++ b/.github/PULL_REQUEST_TEMPLATE/config.yml @@ -1,2 +1 @@ -blank_issues_enabled: false blank_pull_request_template_enabled: false diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index 83a365eab9d8e..0000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,22 +0,0 @@ -## Description - - - - -Fixes # (issue) - - -## How Has This Been Tested? - - - -- [ ] Test A -- [ ] Test B - -## Screenshots (if appropriate): - - -## Checklist: - -- [ ] I have performed a self-review of my own code -- [ ] I have made corresponding changes to the documentation if applicable \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000..5d4290fd7bdb9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ +## Description + + + + + +Fixes # (issue) + +## How Has This Been Tested? + + + +- [ ] Test A +- [ ] Test B + +

Screenshots (if appropriate)

+ + + +
+ + + +## Checklist: + +- [ ] I have performed a self-review of my own code +- [ ] I have made corresponding changes to the documentation if applicable +- [ ] I have no unrelated changes in the PR. +- [ ] I have confirmed that any new dependencies are strictly necessary. +- [ ] I have written tests for new code (if applicable) +- [ ] I have followed naming conventions/patterns in the surrounding code +- [ ] All code in `src/services` uses repositories implementations for database calls, filesystem operations, etc. +- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services`) From 58bf58b393ff0c83f7c0908cace689ef22e443db Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 4 Feb 2025 16:07:41 +0100 Subject: [PATCH 3/8] refactor: get map markers database query (#15899) --- server/src/queries/map.repository.sql | 24 +++++++++++++ server/src/repositories/map.repository.ts | 41 ++++++++++------------- 2 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 server/src/queries/map.repository.sql diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql new file mode 100644 index 0000000000000..d48c420462520 --- /dev/null +++ b/server/src/queries/map.repository.sql @@ -0,0 +1,24 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- MapRepository.getMapMarkers +select + "id", + "exif"."latitude" as "lat", + "exif"."longitude" as "lon", + "exif"."city", + "exif"."state", + "exif"."country" +from + "assets" + inner join "exif" on "assets"."id" = "exif"."assetId" + and "exif"."latitude" is not null + and "exif"."longitude" is not null + left join "albums_assets_assets" on "assets"."id" = "albums_assets_assets"."assetsId" +where + "isVisible" = $1 + and "deletedAt" is null + and "exif"."latitude" is not null + and "exif"."longitude" is not null + and "ownerId" in ($2) +order by + "fileCreatedAt" desc diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index af24b0c94ea7f..fecc9a4f5235e 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -8,7 +8,7 @@ import { readFile } from 'node:fs/promises'; import readLine from 'node:readline'; import { citiesFile } from 'src/constants'; import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; -import { AssetEntity, withExif } from 'src/entities/asset.entity'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; import { LogLevel, SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -76,17 +76,19 @@ export class MapRepository { this.logger.log('Geodata import completed'); } - async getMapMarkers( - ownerIds: string[], - albumIds: string[], - options: MapMarkerSearchOptions = {}, - ): Promise { + @GenerateSql({ params: [[DummyValue.UUID], []] }) + getMapMarkers(ownerIds: string[], albumIds: string[], options: MapMarkerSearchOptions = {}) { const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; - const assets = (await this.db + return this.db .selectFrom('assets') - .$call(withExif) - .select('id') + .innerJoin('exif', (builder) => + builder + .onRef('assets.id', '=', 'exif.assetId') + .on('exif.latitude', 'is not', null) + .on('exif.longitude', 'is not', null), + ) + .select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country']) .leftJoin('albums_assets_assets', (join) => join.onRef('assets.id', '=', 'albums_assets_assets.assetsId')) .where('isVisible', '=', true) .$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!)) @@ -96,30 +98,21 @@ export class MapRepository { .where('deletedAt', 'is', null) .where('exif.latitude', 'is not', null) .where('exif.longitude', 'is not', null) - .where((eb) => { - const ors: Expression[] = []; + .where((builder) => { + const expression: Expression[] = []; if (ownerIds.length > 0) { - ors.push(eb('ownerId', 'in', ownerIds)); + expression.push(builder('ownerId', 'in', ownerIds)); } if (albumIds.length > 0) { - ors.push(eb('albums_assets_assets.albumsId', 'in', albumIds)); + expression.push(builder('albums_assets_assets.albumsId', 'in', albumIds)); } - return eb.or(ors); + return builder.or(expression); }) .orderBy('fileCreatedAt', 'desc') - .execute()) as any as AssetEntity[]; - - return assets.map((asset) => ({ - id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, - })); + .execute() as Promise; } async reverseGeocode(point: GeoPoint): Promise { From fe42e7410bfd12053debbcdb9a4feba9c9d00fbc Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 16:57:11 -0600 Subject: [PATCH 4/8] chore(server): follow up on #15899 (#15907) --- server/src/queries/map.repository.sql | 2 -- server/src/repositories/map.repository.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql index d48c420462520..8b508b68efed5 100644 --- a/server/src/queries/map.repository.sql +++ b/server/src/queries/map.repository.sql @@ -17,8 +17,6 @@ from where "isVisible" = $1 and "deletedAt" is null - and "exif"."latitude" is not null - and "exif"."longitude" is not null and "ownerId" in ($2) order by "fileCreatedAt" desc diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index fecc9a4f5235e..fcfa74a5d0ab6 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -96,8 +96,6 @@ export class MapRepository { .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) .where('deletedAt', 'is', null) - .where('exif.latitude', 'is not', null) - .where('exif.longitude', 'is not', null) .where((builder) => { const expression: Expression[] = []; From 1d6a4e9318c80006f51b30a17a1be6b7ef87ebaf Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 5 Feb 2025 16:20:46 +0100 Subject: [PATCH 5/8] fix: call hexOrBufferToBase64 for stripMetadata thumbhash (#15917) Fixes #15916 (I think) --- server/src/dtos/asset-response.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 0658567912a35..9a963e1e98097 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -118,7 +118,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As id: entity.id, type: entity.type, originalMimeType: mimeTypes.lookup(entity.originalFileName), - thumbhash: entity.thumbhash?.toString('base64') ?? null, + thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, localDateTime: entity.localDateTime, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, From 1492b55c07ae9387e5f8ff7e29fcc07e88f9ecbe Mon Sep 17 00:00:00 2001 From: defooster <64151343+defooster@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:35:55 -0500 Subject: [PATCH 6/8] fix(docs): typo in unraid.md (#15913) Update unraid.md fixed wrong word --- docs/docs/install/unraid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index d6dde4e8c584e..776eef7eb93eb 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -72,7 +72,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich" -5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**" +5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**" 6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**" 7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following: From 48d421e28c6acab27c2203fb7cb5ffdb574252bf Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 5 Feb 2025 19:47:27 +0100 Subject: [PATCH 7/8] fix(server): always get UTC dates from postgres (#15920) --- server/src/repositories/config.repository.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index a2af1b61b35c5..5b04914dac2a1 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -227,6 +227,7 @@ const getEnv = (): EnvData => { } const driverOptions = { + ...parsedOptions, onnotice: (notice: Notice) => { if (notice['severity'] !== 'NOTICE') { console.warn('Postgres notice:', notice); @@ -247,7 +248,9 @@ const getEnv = (): EnvData => { serialize: (value: number) => value.toString(), }, }, - ...parsedOptions, + connection: { + TimeZone: 'UTC', + }, }; return { From 921ef806b6634419f4eb0478bf5459475592c816 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Thu, 6 Feb 2025 00:14:25 +0100 Subject: [PATCH 8/8] Add placeholder type --- server/src/dtos/asset-response.dto.ts | 10 ---------- server/src/entities/asset.entity.ts | 12 +++++++++--- server/src/repositories/asset.repository.ts | 16 +++++++++++----- server/src/repositories/view-repository.ts | 3 +++ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 2a8cbbb426b77..9a963e1e98097 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -113,16 +113,6 @@ const hexOrBufferToBase64 = (encoded: string | Buffer) => { export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; - if (entity.localDateTime === null) { - throw new Error(`Asset ${entity.id} has no localDateTime`); - } - if (entity.fileCreatedAt === null) { - throw new Error(`Asset ${entity.id} has no fileCreatedAt`); - } - if (entity.fileModifiedAt === null) { - throw new Error(`Asset ${entity.id} has no fileModifiedAt`); - } - if (stripMetadata) { const sanitizedAssetResponse: SanitizedAssetResponseDto = { id: entity.id, diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 742ff67b2d7e3..0677675fdd307 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -101,13 +101,13 @@ export class AssetEntity { @Index('idx_asset_file_created_at') @Column({ type: 'timestamptz', nullable: true, default: null }) - fileCreatedAt!: Date | null; + fileCreatedAt!: Date; @Column({ type: 'timestamptz', nullable: true, default: null }) - localDateTime!: Date | null; + localDateTime!: Date; @Column({ type: 'timestamptz', nullable: true, default: null }) - fileModifiedAt!: Date | null; + fileModifiedAt!: Date; @Column({ type: 'boolean', default: false }) isFavorite!: boolean; @@ -180,6 +180,12 @@ export class AssetEntity { duplicateId!: string | null; } +export type AssetEntityPlaceholder = AssetEntity & { + fileCreatedAt: Date | null; + fileModifiedAt: Date | null; + localDateTime: Date | null; +}; + export function withExif(qb: SelectQueryBuilder) { return qb.leftJoin('exif', 'assets.id', 'exif.assetId').select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index b6bbee83523c3..541fb428bd058 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Updateable, sql } from 'kysely'; +import { Insertable, Kysely, NotNull, Updateable, sql } from 'kysely'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { ASSET_FILE_CONFLICT_KEYS, EXIF_CONFLICT_KEYS, JOB_STATUS_CONFLICT_KEYS } from 'src/constants'; @@ -7,6 +7,7 @@ import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetEntity, + AssetEntityPlaceholder, hasPeople, searchAssetBuilder, truncatedDate, @@ -79,8 +80,12 @@ export class AssetRepository implements IAssetRepository { .execute(); } - create(asset: Insertable): Promise { - return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise; + create(asset: Insertable): Promise { + return this.db + .insertInto('assets') + .values(asset) + .returningAll() + .executeTakeFirst() as any as Promise; } @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) @@ -131,6 +136,9 @@ export class AssetRepository implements IAssetRepository { ) .innerJoin('exif', 'a.id', 'exif.assetId') .selectAll('a') + .$narrowType<{ fileCreatedAt: NotNull }>() + .$narrowType<{ fileModifiedAt: NotNull }>() + .$narrowType<{ localDateTime: NotNull }>() .select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')), ) .selectFrom('res') @@ -838,8 +846,6 @@ export class AssetRepository implements IAssetRepository { .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) .where('assets.ownerId', '=', anyUuid(options.userIds)) .where('isVisible', '=', true) - .where('assets.fileCreatedAt', 'is not', null) - .where('assets.fileModifiedAt', 'is not', null) .where('updatedAt', '>', options.updatedAfter) .limit(options.limit) .execute() as any as Promise; diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts index f24b1bac6e4d0..932daf8868813 100644 --- a/server/src/repositories/view-repository.ts +++ b/server/src/repositories/view-repository.ts @@ -37,6 +37,9 @@ export class ViewRepository { .where('deletedAt', 'is', null) .where('originalPath', 'like', `%${normalizedPath}/%`) .where('originalPath', 'not like', `%${normalizedPath}/%/%`) + .$narrowType<{ fileCreatedAt: Date }>() + .$narrowType<{ fileModifiedAt: Date }>() + .$narrowType<{ localDateTime: Date }>() .orderBy( (eb) => eb.fn('regexp_replace', ['assets.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]), 'asc',