fix(server): include album owner for album shared links (#31990)

fix(server): include album owner for album shared links (#31837)

(cherry picked from commit fd97f3e5a62a4af1a8e17d2e766eb952eefae24a)

Co-authored-by: Mees Frensel <33722705+meesfrensel@users.noreply.github.com>
This commit is contained in:
immich-push-o-matic[bot]
2026-10-01 17:06:45 +00:00
committed by GitHub
co-authored by Mees Frensel
parent 882da7cab8
commit 843051f0f5
3 changed files with 87 additions and 69 deletions
+64 -43
View File
@@ -64,6 +64,40 @@ from
left join lateral (
select
"album".*,
(
select
coalesce(json_agg(agg), '[]')
from
(
select
"album_user"."role",
(
select
to_json(obj)
from
(
select
"id",
"name",
"email",
"avatarColor",
"profileImagePath",
"profileChangedAt"
from
(
select
1
) as "dummy"
) as obj
) as "user"
from
"album_user"
inner join "user" on "user"."id" = "album_user"."userId"
where
"album_user"."role" = 'owner'
and "album_user"."albumId" = "album"."id"
) as agg
) as "albumUsers",
coalesce(
json_agg(
"assets"
@@ -74,8 +108,7 @@ from
"assets"."id" is not null
),
'[]'
) as "assets",
to_json("owner") as "owner"
) as "assets"
from
"album"
left join "album_asset" on "album_asset"."albumId" = "album"."id"
@@ -128,34 +161,11 @@ from
order by
"asset"."fileCreatedAt" asc
) as "assets" on true
inner join lateral (
select
"id",
"name",
"email",
"avatarColor",
"profileImagePath",
"profileChangedAt"
from
"user"
where
exists (
select
from
"album_user"
where
"album_user"."role" = 'owner'
and "album_user"."albumId" = "album"."id"
and "album_user"."userId" = "user"."id"
)
and "user"."deletedAt" is null
) as "owner" on true
where
"album"."id" = "shared_link"."albumId"
and "album"."deletedAt" is null
group by
"album"."id",
"owner".*
"album"."id"
) as "album" on true
where
"shared_link"."id" = $1
@@ -195,31 +205,42 @@ from
left join lateral (
select
"album".*,
to_json("owner") as "owner"
from
"album"
inner join lateral (
(
select
"id",
"name",
"email",
"avatarColor",
"profileImagePath",
"profileChangedAt"
coalesce(json_agg(agg), '[]')
from
"user"
where
exists (
(
select
"album_user"."role",
(
select
to_json(obj)
from
(
select
"id",
"name",
"email",
"avatarColor",
"profileImagePath",
"profileChangedAt"
from
(
select
1
) as "dummy"
) as obj
) as "user"
from
"album_user"
inner join "user" on "user"."id" = "album_user"."userId"
where
"album_user"."role" = 'owner'
and "album_user"."albumId" = "album"."id"
and "album_user"."userId" = "user"."id"
)
and "user"."deletedAt" is null
) as "owner" on true
) as agg
) as "albumUsers"
from
"album"
where
"album"."id" = "shared_link"."albumId"
and "album"."deletedAt" is null
@@ -18,6 +18,7 @@ import { DB } from 'src/schema/index.js';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table.js';
import { AssetTable } from 'src/schema/tables/asset.table.js';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table.js';
import { dummy } from 'src/utils/database.js';
export type SharedLinkSearchOptions = {
userId: string;
@@ -43,22 +44,18 @@ export const withExifInfo = (eb: ExpressionBuilder<DB, 'asset'>) => {
.as('exifInfo');
};
const withAlbumOwner = (eb: ExpressionBuilder<DB, 'album'>) => {
return eb
.selectFrom('user')
.select(columns.user)
.where((eb) =>
eb.exists(
eb
.selectFrom('album_user')
.where('album_user.role', '=', sql.lit(AlbumUserRole.Owner))
.whereRef('album_user.albumId', '=', 'album.id')
.whereRef('album_user.userId', '=', 'user.id'),
),
)
.where('user.deletedAt', 'is', null)
.as('owner');
};
const withAlbumOwner = (eb: ExpressionBuilder<DB, 'album'>) =>
jsonArrayFrom(
eb
.selectFrom('album_user')
.innerJoin('user', 'user.id', 'album_user.userId')
.where('album_user.role', '=', sql.lit(AlbumUserRole.Owner))
.whereRef('album_user.albumId', '=', 'album.id')
.select('album_user.role')
.select((eb) => jsonObjectFrom(eb.selectFrom(dummy).select(columns.user)).$notNull().as('user')),
)
.$notNull()
.as('albumUsers');
const withSharedLinkAlbum = (eb: ExpressionBuilder<DB, 'shared_link'>) => {
return eb
@@ -101,7 +98,7 @@ export class SharedLinkRepository {
.as('assets'),
(join) => join.onTrue(),
)
.innerJoinLateral(withAlbumOwner, (join) => join.onTrue())
.select(withAlbumOwner)
.select((eb) =>
eb.fn
.coalesce(
@@ -114,8 +111,7 @@ export class SharedLinkRepository {
)
.as('assets'),
)
.select((eb) => eb.fn.toJson('owner').as('owner'))
.groupBy(['album.id', sql`"owner".*`])
.groupBy('album.id')
.as('album'),
(join) => join.onTrue(),
)
@@ -135,11 +131,7 @@ export class SharedLinkRepository {
.select((eb) => jsonArrayFrom(withSharedAssets(eb).limit(1)).as('assets'))
.where('shared_link.userId', '=', userId)
.leftJoinLateral(
(eb) =>
withSharedLinkAlbum(eb)
.innerJoinLateral(withAlbumOwner, (join) => join.onTrue())
.select((eb) => eb.fn.toJson('owner').as('owner'))
.as('album'),
(eb) => withSharedLinkAlbum(eb).select(withAlbumOwner).as('album'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('album').$castTo<ShallowDehydrateObject<Album> | null>().as('album'))
@@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import { randomBytes } from 'node:crypto';
import { SharedLinkType } from 'src/enum.js';
import { AlbumUserRole, SharedLinkType } from 'src/enum.js';
import { AccessRepository } from 'src/repositories/access.repository.js';
import { DatabaseRepository } from 'src/repositories/database.repository.js';
import { LoggingRepository } from 'src/repositories/logging.repository.js';
@@ -236,12 +236,14 @@ describe(SharedLinkService.name, () => {
expect(result[0].id).toBe(link1.id);
});
it('should return album shared links with album data', async () => {
it('should return album shared links with album data and (only) the owner', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: albumContributor } = await ctx.newUser();
const auth = factory.auth({ user });
const { album } = await ctx.newAlbum({ ownerId: user.id });
await ctx.newAlbumUser({ albumId: album.id, userId: albumContributor.id, role: AlbumUserRole.Editor });
const sharedLinkRepo = ctx.get(SharedLinkRepository);
@@ -258,6 +260,9 @@ describe(SharedLinkService.name, () => {
expect(result).toHaveLength(1);
expect(result[0].album).toBeDefined();
expect(result[0].album!.id).toBe(album.id);
expect(result[0].album!.albumUsers.length).toBe(1);
expect(result[0].album!.albumUsers[0].role).toBe(AlbumUserRole.Owner);
expect(result[0].album!.albumUsers[0].user.id).toBe(user.id);
});
it('should return multiple album shared links without sql error from json group by', async () => {