From 4c2d2284aa51ace226ebab6c6f44bffe0fc07f15 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 15 Jul 2026 20:15:41 +0200 Subject: [PATCH] refactor(server): album user route params (#29503) --- mobile/openapi/lib/api/albums_api.dart | 8 ++++++++ open-api/immich-openapi-specs.json | 18 ++++++++++++++++++ server/src/controllers/album.controller.ts | 12 ++++-------- server/src/dtos/album.dto.ts | 14 ++++++++++++++ server/src/validation.ts | 12 +----------- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e6e7cdbf40..6ac978f701 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -681,8 +681,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): + /// Album ID /// /// * [String] userId (required): + /// Album user ID, or \"me\" to reference the current user. Future removeUserFromAlbumWithHttpInfo(String id, String userId, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/user/{userId}' @@ -718,8 +720,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): + /// Album ID /// /// * [String] userId (required): + /// Album user ID, or \"me\" to reference the current user. Future removeUserFromAlbum(String id, String userId, { Future? abortTrigger, }) async { final response = await removeUserFromAlbumWithHttpInfo(id, userId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { @@ -798,8 +802,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): + /// Album ID /// /// * [String] userId (required): + /// Album user ID, or \"me\" to reference the current user. /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): Future updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { @@ -837,8 +843,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): + /// Album ID /// /// * [String] userId (required): + /// Album user ID, or \"me\" to reference the current user. /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b4158b34a3..e73760a3de 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2702,6 +2702,7 @@ "name": "id", "required": true, "in": "path", + "description": "Album ID", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", @@ -2712,6 +2713,14 @@ "name": "userId", "required": true, "in": "path", + "description": "Album user ID, or \"me\" to reference the current user.", + "x-immich-history": [ + { + "version": "v3", + "state": "Updated", + "description": "\"me\" as a value is deprecated" + } + ], "schema": { "type": "string" } @@ -2762,6 +2771,7 @@ "name": "id", "required": true, "in": "path", + "description": "Album ID", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", @@ -2772,6 +2782,14 @@ "name": "userId", "required": true, "in": "path", + "description": "Album user ID, or \"me\" to reference the current user.", + "x-immich-history": [ + { + "version": "v3", + "state": "Updated", + "description": "\"me\" as a value is deprecated" + } + ], "schema": { "type": "string" } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 90a8fa5a25..4101309dbb 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -7,6 +7,7 @@ import { AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, AlbumStatisticsResponseDto, + AlbumUserParamDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -18,7 +19,7 @@ import { MapMarkerResponseDto } from 'src/dtos/map.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; -import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; +import { UUIDParamDto } from 'src/validation'; @ApiTags(ApiTag.Albums) @Controller('albums') @@ -175,8 +176,7 @@ export class AlbumController { }) updateAlbumUser( @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, + @Param() { id, userId }: AlbumUserParamDto, @Body() dto: UpdateAlbumUserDto, ): Promise { return this.service.updateUser(auth, id, userId, dto); @@ -190,11 +190,7 @@ export class AlbumController { description: 'Remove a user from an album. Use an ID of "me" to leave a shared album.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - removeUserFromAlbum( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, - ): Promise { + removeUserFromAlbum(@Auth() auth: AuthDto, @Param() { id, userId }: AlbumUserParamDto): Promise { return this.service.removeUser(auth, id, userId); } } diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index a66f912972..3c871f672d 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,6 +1,7 @@ import { ShallowDehydrateObject } from 'kysely'; import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink } from 'src/database'; +import { HistoryBuilder } from 'src/decorators'; import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; @@ -140,6 +141,19 @@ export const AlbumResponseSchema = z }) .meta({ id: 'AlbumResponseDto' }); +const AlbumUserParamSchema = z.object({ + id: z.uuidv4().describe('Album ID'), + // TODO: disallow 'me' as a shortcut in v4 and type userId as uuidv4 + userId: z + .string() + .refine((value) => value === 'me' || z.uuidv4().safeParse(value).success, { + error: 'Must be a UUID v4 or "me"', + }) + .describe('Album user ID, or "me" to reference the current user.') + .meta(new HistoryBuilder().updated('v3', '"me" as a value is deprecated').getExtensions()), +}); + +export class AlbumUserParamDto extends createZodDto(AlbumUserParamSchema) {} export class AddUsersDto extends createZodDto(AddUsersSchema) {} export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} diff --git a/server/src/validation.ts b/server/src/validation.ts index 419d0cb0dc..79d249b1c9 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -1,4 +1,4 @@ -import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common'; +import { FileValidator, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { createZodDto } from 'nestjs-zod'; import sanitize from 'sanitize-filename'; @@ -75,16 +75,6 @@ export function IsNotSiblingOf< ); } -@Injectable() -export class ParseMeUUIDPipe extends ParseUUIDPipe { - async transform(value: string, metadata: ArgumentMetadata) { - if (value == 'me') { - return value; - } - return super.transform(value, metadata); - } -} - @Injectable() export class FileNotEmptyValidator extends FileValidator { constructor(private requiredFields: string[]) {