From b4cc406a3f3d752419bb9b55fcde21f280c54526 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:00:02 +0200 Subject: [PATCH] fix!: search endpoints visibility can be omitted (#29385) --- server/src/queries/search.repository.sql | 61 +++++++++---------- server/src/repositories/search.repository.ts | 7 ++- server/src/services/search.service.spec.ts | 2 +- server/src/services/search.service.ts | 18 +++++- server/src/utils/database.ts | 7 ++- .../specs/services/search.service.spec.ts | 29 +++++++++ server/test/small.factory.ts | 6 +- 7 files changed, 86 insertions(+), 44 deletions(-) diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 3e75d88af..25b856637 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -7,18 +7,17 @@ from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "asset"."visibility" = $1 - and "asset"."fileCreatedAt" >= $2 - and "asset_exif"."lensModel" = $3 - and "asset"."ownerId" = any ($4::uuid[]) - and "asset"."isFavorite" = $5 + "asset"."fileCreatedAt" >= $1 + and "asset_exif"."lensModel" = $2 + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."isFavorite" = $4 and "asset"."deletedAt" is null order by "asset"."fileCreatedAt" desc limit - $6 + $5 offset - $7 + $6 -- SearchRepository.searchStatistics select @@ -27,11 +26,10 @@ from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "asset"."visibility" = $1 - and "asset"."fileCreatedAt" >= $2 - and "asset_exif"."lensModel" = $3 - and "asset"."ownerId" = any ($4::uuid[]) - and "asset"."isFavorite" = $5 + "asset"."fileCreatedAt" >= $1 + and "asset_exif"."lensModel" = $2 + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."isFavorite" = $4 and "asset"."deletedAt" is null -- SearchRepository.searchRandom @@ -41,16 +39,15 @@ from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "asset"."visibility" = $1 - and "asset"."fileCreatedAt" >= $2 - and "asset_exif"."lensModel" = $3 - and "asset"."ownerId" = any ($4::uuid[]) - and "asset"."isFavorite" = $5 + "asset"."fileCreatedAt" >= $1 + and "asset_exif"."lensModel" = $2 + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."isFavorite" = $4 and "asset"."deletedAt" is null order by random() limit - $6 + $5 -- SearchRepository.searchLargeAssets select @@ -60,17 +57,16 @@ from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "asset"."visibility" = $1 - and "asset"."fileCreatedAt" >= $2 - and "asset_exif"."lensModel" = $3 - and "asset"."ownerId" = any ($4::uuid[]) - and "asset"."isFavorite" = $5 + "asset"."fileCreatedAt" >= $1 + and "asset_exif"."lensModel" = $2 + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."isFavorite" = $4 and "asset"."deletedAt" is null - and "asset_exif"."fileSizeInByte" > $6 + and "asset_exif"."fileSizeInByte" > $5 order by "asset_exif"."fileSizeInByte" desc limit - $7 + $6 -- SearchRepository.searchSmart begin @@ -83,18 +79,17 @@ from inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" inner join "smart_search" on "asset"."id" = "smart_search"."assetId" where - "asset"."visibility" = $1 - and "asset"."fileCreatedAt" >= $2 - and "asset_exif"."lensModel" = $3 - and "asset"."ownerId" = any ($4::uuid[]) - and "asset"."isFavorite" = $5 + "asset"."fileCreatedAt" >= $1 + and "asset_exif"."lensModel" = $2 + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."isFavorite" = $4 and "asset"."deletedAt" is null order by - smart_search.embedding <=> $6 + smart_search.embedding <=> $5 limit - $7 + $6 offset - $8 + $7 commit -- SearchRepository.getEmbedding diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index da3f31555..7faa19f7c 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -117,7 +117,8 @@ type BaseAssetSearchOptions = SearchDateOptions & SearchAlbumOptions & SearchOcrOptions; -export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; +export type AssetSearchOptions = Omit & + SearchRelationOptions & { visibility?: AssetVisibility | 'not-locked' }; export type AssetSearchBuilderOptions = Omit; @@ -125,11 +126,11 @@ export type SmartSearchOptions = SearchDateOptions & SearchEmbeddingOptions & SearchExifOptions & SearchOneToOneRelationOptions & - SearchStatusOptions & + Omit & SearchUserIdOptions & SearchPeopleOptions & SearchTagOptions & - SearchOcrOptions; + SearchOcrOptions & { visibility?: AssetVisibility | 'not-locked' }; export type OcrSearchOptions = SearchDateOptions & SearchOcrOptions; diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 4680b4c9d..c0f5a3614 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -250,7 +250,7 @@ describe(SearchService.name, () => { ); expect(mocks.search.searchSmart).toHaveBeenCalledWith( { page: 1, size: 100 }, - { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] }, + { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' }, ); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index bd50ab308..37facf325 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -81,6 +81,7 @@ export class SearchService extends BaseService { { ...dto, checksum, + visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, orderDirection: dto.order ?? AssetOrder.Desc, }, @@ -91,9 +92,13 @@ export class SearchService extends BaseService { async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise { const userIds = await this.getUserIdsToSearch(auth); + if (dto.visibility === AssetVisibility.Locked) { + requireElevatedPermission(auth); + } return await this.searchRepository.searchStatistics({ ...dto, + visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, }); } @@ -114,7 +119,11 @@ export class SearchService extends BaseService { } const userIds = await this.getUserIdsToSearch(auth, dto.visibility); - const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds }); + const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { + ...dto, + visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), + userIds, + }); return items.map((item) => mapAsset(item, { auth })); } @@ -155,7 +164,12 @@ export class SearchService extends BaseService { const size = dto.size || 100; const { hasNextPage, items } = await this.searchRepository.searchSmart( { page, size }, - { ...dto, userIds: await userIds, embedding }, + { + ...dto, + userIds: await userIds, + embedding, + visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), + }, ); return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index a187c2591..5c2c0da16 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -373,12 +373,15 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuilderOptions) { options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); - const visibility = options.visibility == null ? AssetVisibility.Timeline : options.visibility; return kysely .withPlugin(joinDeduplicationPlugin) .selectFrom('asset') - .where('asset.visibility', '=', visibility) + .$if(!!options.visibility, (qb) => + options.visibility === 'not-locked' + ? qb.where('asset.visibility', '!=', AssetVisibility.Locked) + : qb.where('asset.visibility', '=', options.visibility!), + ) .$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(options.tagIds === null, (qb) => diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index 18e03b2e4..4ed8fe8c2 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely'; import { SearchSuggestionType } from 'src/dtos/search.dto'; +import { AssetVisibility } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; @@ -108,6 +109,34 @@ describe(SearchService.name, () => { expect(response.assets.items.length).toBe(1); expect(response.assets.items[0].id).toBe(unstackedAsset.id); }); + + describe('visibility', () => { + it('should filter out locked assets in a default session', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked }); + + const auth = factory.auth({ user: { id: user.id } }); + + const response = await sut.searchMetadata(auth, { withStacked: false }); + + expect(response.assets.items.length).toBe(0); + }); + + it('should return locked assets in an elevated session', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked }); + + const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } }); + + const response = await sut.searchMetadata(auth, { withStacked: false }); + + expect(response.assets.items.length).toBe(1); + }); + }); }); describe('getSearchSuggestions', () => { diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 5a1a8310b..42b7333dd 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -27,7 +27,7 @@ const authFactory = ({ user, }: { apiKey?: Partial; - session?: { id: string }; + session?: { id?: string; hasElevatedPermission?: boolean }; user?: Omit< Partial, 'createdAt' | 'updatedAt' | 'deletedAt' | 'fileCreatedAt' | 'fileModifiedAt' | 'localDateTime' | 'profileChangedAt' @@ -46,8 +46,8 @@ const authFactory = ({ if (session) { auth.session = { - id: session.id, - hasElevatedPermission: false, + id: session.id ?? newUuid(), + hasElevatedPermission: session.hasElevatedPermission ?? false, }; }