diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index 434fa2b7aa..f58a53723a 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -59,6 +59,13 @@ describe(APIKeyController.name, () => { expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); }); + + it('should allow updating just the name', async () => { + const { status } = await request(ctx.getHttpServer()) + .put(`/api-keys/${factory.uuid()}`) + .send({ name: 'new name' }); + expect(status).toBe(200); + }); }); describe('DELETE /api-keys/:id', () => { diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index d11fe8da7e..4b11a16e14 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { Activity } from 'src/database'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { Optional, ValidateUUID } from 'src/validation'; +import { ValidateEnum, ValidateUUID } from 'src/validation'; export enum ReactionType { COMMENT = 'comment', @@ -19,7 +19,7 @@ export type MaybeDuplicate = { duplicate: boolean; value: T }; export class ActivityResponseDto { id!: string; createdAt!: Date; - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) type!: ReactionType; user!: UserResponseDto; assetId!: string | null; @@ -43,14 +43,10 @@ export class ActivityDto { } export class ActivitySearchDto extends ActivityDto { - @IsEnum(ReactionType) - @Optional() - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true }) type?: ReactionType; - @IsEnum(ReactionLevel) - @Optional() - @ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel }) + @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true }) level?: ReactionLevel; @ValidateUUID({ optional: true }) @@ -60,8 +56,7 @@ export class ActivitySearchDto extends ActivityDto { const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; export class ActivityCreateDto extends ActivityDto { - @IsEnum(ReactionType) - @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) type!: ReactionType; @ValidateIf(isComment) diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 40e51ef729..c6cde0894f 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,13 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AlbumUserRole, AssetOrder } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class AlbumInfoDto { @ValidateBoolean({ optional: true }) @@ -18,8 +18,7 @@ export class AlbumUserAddDto { @ValidateUUID() userId!: string; - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) role?: AlbumUserRole; } @@ -32,8 +31,7 @@ export class AlbumUserCreateDto { @ValidateUUID() userId!: string; - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } @@ -71,9 +69,7 @@ export class UpdateAlbumDto { @ValidateBoolean({ optional: true }) isActivityEnabled?: boolean; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) order?: AssetOrder; } @@ -107,14 +103,13 @@ export class AlbumStatisticsResponseDto { } export class UpdateAlbumUserDto { - @IsEnum(AlbumUserRole) - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } export class AlbumUserResponseDto { user!: UserResponseDto; - @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } @@ -137,8 +132,7 @@ export class AlbumResponseDto { startDate?: Date; endDate?: Date; isActivityEnabled!: boolean; - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) order?: AssetOrder; } diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index c790ea613d..c9475fa2b1 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,15 +1,13 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; import { Permission } from 'src/enum'; -import { Optional } from 'src/validation'; +import { Optional, ValidateEnum } from 'src/validation'; export class APIKeyCreateDto { @IsString() @IsNotEmpty() @Optional() name?: string; - @IsEnum(Permission, { each: true }) - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) @ArrayMinSize(1) permissions!: Permission[]; } @@ -20,9 +18,7 @@ export class APIKeyUpdateDto { @IsNotEmpty() name?: string; - @Optional() - @IsEnum(Permission, { each: true }) - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true }) @ArrayMinSize(1) permissions?: Permission[]; } @@ -37,6 +33,6 @@ export class APIKeyResponseDto { name!: string; createdAt!: Date; updatedAt!: Date; - @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) permissions!: Permission[]; } diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 5cd9b7e7d9..887762dbdd 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ValidateEnum } from 'src/validation'; export enum AssetMediaStatus { CREATED = 'created', @@ -6,7 +6,7 @@ export enum AssetMediaStatus { DUPLICATE = 'duplicate', } export class AssetMediaResponseDto { - @ApiProperty({ enum: AssetMediaStatus, enumName: 'AssetMediaStatus' }) + @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' }) status!: AssetMediaStatus; id!: string; } diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 92e1302864..ea86e087d8 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { /** @@ -15,9 +15,7 @@ export enum AssetMediaSize { } export class AssetMediaOptionsDto { - @Optional() - @IsEnum(AssetMediaSize) - @ApiProperty({ enumName: 'AssetMediaSize', enum: AssetMediaSize }) + @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true }) size?: AssetMediaSize; } @@ -60,7 +58,7 @@ export class AssetMediaCreateDto extends AssetMediaBase { @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateUUID({ optional: true }) diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 1e214c3860..5b587e59ba 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -15,10 +15,11 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; +import { ValidateEnum } from 'src/validation'; export class SanitizedAssetResponseDto { id!: string; - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) type!: AssetType; thumbhash!: string | null; originalMimeType?: string; @@ -72,7 +73,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { isArchived!: boolean; isTrashed!: boolean; isOffline!: boolean; - @ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) visibility!: AssetVisibility; exifInfo?: ExifResponseDto; tags?: TagResponseDto[]; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 940cfbf9cc..727ab1625d 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsDateString, - IsEnum, IsInt, IsLatitude, IsLongitude, @@ -16,7 +15,7 @@ import { import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AssetType, AssetVisibility } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class DeviceIdDto { @IsNotEmpty() @@ -32,7 +31,7 @@ export class UpdateAssetBase { @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @Optional() @@ -99,13 +98,12 @@ export enum AssetJobName { } export class AssetJobsDto extends AssetIdsDto { - @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) - @IsEnum(AssetJobName) + @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' }) name!: AssetJobName; } export class AssetStatsDto { - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateBoolean({ optional: true }) diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index ce6aad4c06..60124c877a 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,19 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; import { JobCommand, ManualJobName, QueueName } from 'src/enum'; -import { ValidateBoolean } from 'src/validation'; +import { ValidateBoolean, ValidateEnum } from 'src/validation'; export class JobIdParamDto { - @IsNotEmpty() - @IsEnum(QueueName) - @ApiProperty({ type: String, enum: QueueName, enumName: 'JobName' }) + @ValidateEnum({ enum: QueueName, name: 'JobName' }) id!: QueueName; } export class JobCommandDto { - @IsNotEmpty() - @IsEnum(JobCommand) - @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) + @ValidateEnum({ enum: JobCommand, name: 'JobCommand' }) command!: JobCommand; @ValidateBoolean({ optional: true }) @@ -21,8 +16,7 @@ export class JobCommandDto { } export class JobCreateDto { - @IsEnum(ManualJobName) - @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' }) + @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' }) name!: ManualJobName; } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 675039363b..e92e11bdfb 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class MemoryBaseDto { @ValidateBoolean({ optional: true }) @@ -16,9 +16,7 @@ class MemoryBaseDto { } export class MemorySearchDto { - @Optional() - @IsEnum(MemoryType) - @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true }) type?: MemoryType; @ValidateDate({ optional: true }) @@ -45,8 +43,7 @@ export class MemoryUpdateDto extends MemoryBaseDto { } export class MemoryCreateDto extends MemoryBaseDto { - @IsEnum(MemoryType) - @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) type!: MemoryType; @IsObject() @@ -86,7 +83,7 @@ export class MemoryResponseDto { showAt?: Date; hideAt?: Date; ownerId!: string; - @ApiProperty({ enumName: 'MemoryType', enum: MemoryType }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) type!: MemoryType; data!: MemoryData; isSaved!: boolean; diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index d9847cda17..e83ba7315f 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,7 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class TestEmailResponseDto { messageId!: string; @@ -19,9 +18,9 @@ export class NotificationDto { id!: string; @ValidateDate() createdAt!: Date; - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' }) level!: NotificationLevel; - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType' }) type!: NotificationType; title!: string; description?: string; @@ -30,18 +29,13 @@ export class NotificationDto { } export class NotificationSearchDto { - @Optional() @ValidateUUID({ optional: true }) id?: string; - @IsEnum(NotificationLevel) - @Optional() - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) level?: NotificationLevel; - @IsEnum(NotificationType) - @Optional() - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) type?: NotificationType; @ValidateBoolean({ optional: true }) @@ -49,14 +43,10 @@ export class NotificationSearchDto { } export class NotificationCreateDto { - @Optional() - @IsEnum(NotificationLevel) - @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) level?: NotificationLevel; - @IsEnum(NotificationType) - @Optional() - @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) type?: NotificationType; @IsString() diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index 0028fca006..47a3992784 100644 --- a/server/src/dtos/onboarding.dto.ts +++ b/server/src/dtos/onboarding.dto.ts @@ -1,8 +1,7 @@ -import { IsBoolean, IsNotEmpty } from 'class-validator'; +import { ValidateBoolean } from 'src/validation'; export class OnboardingDto { - @IsBoolean() - @IsNotEmpty() + @ValidateBoolean() isOnboarded!: boolean; } diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 9d86415dc3..28d4adf8b7 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty } from 'class-validator'; +import { IsNotEmpty } from 'class-validator'; import { UserResponseDto } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; +import { ValidateEnum } from 'src/validation'; export class UpdatePartnerDto { @IsNotEmpty() @@ -9,8 +9,7 @@ export class UpdatePartnerDto { } export class PartnerSearchDto { - @IsEnum(PartnerDirection) - @ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' }) + @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' }) direction!: PartnerDirection; } diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 18da39b6d4..f9b41627d9 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -14,6 +14,7 @@ import { MaxDateString, Optional, ValidateBoolean, + ValidateEnum, ValidateHexColor, ValidateUUID, } from 'src/validation'; @@ -137,7 +138,7 @@ export class AssetFaceWithoutPersonResponseDto { boundingBoxY1!: number; @ApiProperty({ type: 'integer' }) boundingBoxY2!: number; - @ApiProperty({ enum: SourceType, enumName: 'SourceType' }) + @ValidateEnum({ enum: SourceType, name: 'SourceType' }) sourceType?: SourceType; } diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 0024a1b34e..85c6fbf0de 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { Place } from 'src/database'; import { PropertyLifecycle } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class BaseSearchDto { @ValidateUUID({ optional: true, nullable: true }) @@ -17,9 +17,7 @@ class BaseSearchDto { @Optional() deviceId?: string; - @IsEnum(AssetType) - @Optional() - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true }) type?: AssetType; @ValidateBoolean({ optional: true }) @@ -34,7 +32,7 @@ class BaseSearchDto { @ValidateBoolean({ optional: true }) isOffline?: boolean; - @ValidateAssetVisibility({ optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) visibility?: AssetVisibility; @ValidateDate({ optional: true }) @@ -172,9 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto { @Optional() encodedVideoPath?: string; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder, default: AssetOrder.DESC }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.DESC }) order?: AssetOrder; @IsInt() @@ -250,9 +246,7 @@ export enum SearchSuggestionType { } export class SearchSuggestionRequestDto { - @IsEnum(SearchSuggestionType) - @IsNotEmpty() - @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) + @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' }) type!: SearchSuggestionType; @IsString() diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 8d373b40b6..299590c0e3 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import _ from 'lodash'; import { SharedLink } from 'src/database'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class SharedLinkSearchDto { @ValidateUUID({ optional: true }) @@ -13,8 +13,7 @@ export class SharedLinkSearchDto { } export class SharedLinkCreateDto { - @IsEnum(SharedLinkType) - @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) type!: SharedLinkType; @ValidateUUID({ each: true, optional: true }) @@ -90,7 +89,7 @@ export class SharedLinkResponseDto { userId!: string; key!: string; - @ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) type!: SharedLinkType; createdAt!: Date; expiresAt!: Date | null; diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 8ba73271e6..9725539e3d 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, IsEnum, IsInt, IsPositive, IsString } from 'class-validator'; +import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AlbumUserRole, @@ -13,7 +13,7 @@ import { UserMetadataKey, } from 'src/enum'; import { UserMetadata } from 'src/types'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class AssetFullSyncDto { @ValidateUUID({ optional: true }) @@ -90,11 +90,11 @@ export class SyncAssetV1 { fileModifiedAt!: Date | null; localDateTime!: Date | null; duration!: string | null; - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) type!: AssetType; deletedAt!: Date | null; isFavorite!: boolean; - @ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) visibility!: AssetVisibility; livePhotoVideoId!: string | null; stackId!: string | null; @@ -159,7 +159,7 @@ export class SyncAlbumUserDeleteV1 { export class SyncAlbumUserV1 { albumId!: string; userId!: string; - @ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) role!: AlbumUserRole; } @@ -173,7 +173,7 @@ export class SyncAlbumV1 { updatedAt!: Date; thumbnailAssetId!: string | null; isActivityEnabled!: boolean; - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) order!: AssetOrder; } @@ -196,7 +196,7 @@ export class SyncMemoryV1 { updatedAt!: Date; deletedAt!: Date | null; ownerId!: string; - @ApiProperty({ enumName: 'MemoryType', enum: MemoryType }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) type!: MemoryType; data!: object; isSaved!: boolean; @@ -319,8 +319,7 @@ export type SyncItem = { }; export class SyncStreamDto { - @IsEnum(SyncRequestType, { each: true }) - @ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true }) + @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true }) types!: SyncRequestType[]; @ValidateBoolean({ optional: true }) @@ -328,7 +327,7 @@ export class SyncStreamDto { } export class SyncAckDto { - @ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType }) + @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' }) type!: SyncEntityType; ack!: string; } @@ -340,8 +339,6 @@ export class SyncAckSetDto { } export class SyncAckDeleteDto { - @IsEnum(SyncEntityType, { each: true }) - @ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType, isArray: true }) - @Optional() + @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true }) types?: SyncEntityType[]; } diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 49c5e5b4e7..809f381dd6 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -2,8 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Exclude, Transform, Type } from 'class-transformer'; import { ArrayMinSize, - IsBoolean, - IsEnum, IsInt, IsNotEmpty, IsNumber, @@ -34,7 +32,7 @@ import { VideoContainer, } from 'src/enum'; import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean } from 'src/validation'; +import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; @@ -82,24 +80,19 @@ export class SystemConfigFFmpegDto { @IsString() preset!: string; - @IsEnum(VideoCodec) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' }) targetVideoCodec!: VideoCodec; - @IsEnum(VideoCodec, { each: true }) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true }) acceptedVideoCodecs!: VideoCodec[]; - @IsEnum(AudioCodec) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' }) targetAudioCodec!: AudioCodec; - @IsEnum(AudioCodec, { each: true }) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true }) acceptedAudioCodecs!: AudioCodec[]; - @IsEnum(VideoContainer, { each: true }) - @ApiProperty({ enumName: 'VideoContainer', enum: VideoContainer, isArray: true }) + @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true }) acceptedContainers!: VideoContainer[]; @IsString() @@ -131,8 +124,7 @@ export class SystemConfigFFmpegDto { @ValidateBoolean() temporalAQ!: boolean; - @IsEnum(CQMode) - @ApiProperty({ enumName: 'CQMode', enum: CQMode }) + @ValidateEnum({ enum: CQMode, name: 'CQMode' }) cqMode!: CQMode; @ValidateBoolean() @@ -141,19 +133,16 @@ export class SystemConfigFFmpegDto { @IsString() preferredHwDevice!: string; - @IsEnum(TranscodePolicy) - @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) + @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' }) transcode!: TranscodePolicy; - @IsEnum(TranscodeHWAccel) - @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) + @ValidateEnum({ enum: TranscodeHWAccel, name: 'TranscodeHWAccel' }) accel!: TranscodeHWAccel; @ValidateBoolean() accelDecode!: boolean; - @IsEnum(ToneMapping) - @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) + @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' }) tonemap!: ToneMapping; } @@ -264,8 +253,7 @@ class SystemConfigLoggingDto { @ValidateBoolean() enabled!: boolean; - @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) - @IsEnum(LogLevel) + @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) level!: LogLevel; } @@ -306,8 +294,7 @@ enum MapTheme { } export class MapThemeDto { - @IsEnum(MapTheme) - @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) + @ValidateEnum({ enum: MapTheme, name: 'MapTheme' }) theme!: MapTheme; } @@ -368,8 +355,7 @@ class SystemConfigOAuthDto { @IsString() clientSecret!: string; - @IsEnum(OAuthTokenEndpointAuthMethod) - @ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' }) + @ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' }) tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; @IsInt() @@ -431,7 +417,7 @@ class SystemConfigReverseGeocodingDto { } class SystemConfigFacesDto { - @IsBoolean() + @ValidateBoolean() import!: boolean; } @@ -450,12 +436,12 @@ class SystemConfigServerDto { @IsString() loginPageMessage!: string; - @IsBoolean() + @ValidateBoolean() publicUsers!: boolean; } class SystemConfigSmtpTransportDto { - @IsBoolean() + @ValidateBoolean() ignoreCert!: boolean; @IsNotEmpty() @@ -475,7 +461,7 @@ class SystemConfigSmtpTransportDto { } export class SystemConfigSmtpDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @ValidateIf(isEmailNotificationEnabled) @@ -548,8 +534,7 @@ export class SystemConfigThemeDto { } class SystemConfigGeneratedImageDto { - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) format!: ImageFormat; @IsInt() @@ -567,13 +552,10 @@ class SystemConfigGeneratedImageDto { } class SystemConfigGeneratedFullsizeImageDto { - @IsBoolean() - @Type(() => Boolean) - @ApiProperty({ type: 'boolean' }) + @ValidateBoolean() enabled!: boolean; - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) format!: ImageFormat; @IsInt() @@ -600,8 +582,7 @@ export class SystemConfigImageDto { @IsObject() fullsize!: SystemConfigGeneratedFullsizeImageDto; - @IsEnum(Colorspace) - @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) + @ValidateEnum({ enum: Colorspace, name: 'Colorspace' }) colorspace!: Colorspace; @ValidateBoolean() diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index c8e64f2300..0005aee7eb 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -1,11 +1,12 @@ -import { IsBoolean } from 'class-validator'; +import { ValidateBoolean } from 'src/validation'; export class AdminOnboardingUpdateDto { - @IsBoolean() + @ValidateBoolean() isOnboarded!: boolean; } export class AdminOnboardingResponseDto { + @ValidateBoolean() isOnboarded!: boolean; } diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index af2eae7e72..449cec3207 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; import { AssetOrder, AssetVisibility } from 'src/enum'; -import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class TimeBucketDto { @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) @@ -38,16 +38,17 @@ export class TimeBucketDto { @ValidateBoolean({ optional: true, description: 'Include assets shared by partners' }) withPartners?: boolean; - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ + @ValidateEnum({ enum: AssetOrder, - enumName: 'AssetOrder', + name: 'AssetOrder', description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', + optional: true, }) order?: AssetOrder; - @ValidateAssetVisibility({ + @ValidateEnum({ + enum: AssetVisibility, + name: 'AssetVisibility', optional: true, description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', }) @@ -93,10 +94,10 @@ export class TimeBucketAssetResponseDto { }) isFavorite!: boolean[]; - @ApiProperty({ + @ValidateEnum({ enum: AssetVisibility, - enumName: 'AssetVisibility', - isArray: true, + name: 'AssetVisibility', + each: true, description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)', }) visibility!: AssetVisibility[]; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 6765df9f73..d165438061 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,14 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; +import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { AssetOrder, UserAvatarColor } from 'src/enum'; import { UserPreferences } from 'src/types'; -import { Optional, ValidateBoolean } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; class AvatarUpdate { - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true }) color?: UserAvatarColor; } @@ -23,8 +21,7 @@ class RatingsUpdate { } class AlbumsUpdate { - @IsEnum(AssetOrder) - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) defaultAssetOrder?: AssetOrder; } @@ -159,8 +156,7 @@ export class UserPreferencesUpdateDto { } class AlbumsResponse { - @IsEnum(AssetOrder) - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) defaultAssetOrder: AssetOrder = AssetOrder.DESC; } diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index ed08f7534d..3e3a92d42e 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation'; +import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; export class UserUpdateMeDto { @Optional() @@ -23,9 +23,7 @@ export class UserUpdateMeDto { @IsNotEmpty() name?: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; } @@ -34,7 +32,7 @@ export class UserResponseDto { name!: string; email!: string; profileImagePath!: string; - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' }) avatarColor!: UserAvatarColor; profileChangedAt!: Date; } @@ -84,9 +82,7 @@ export class UserAdminCreateDto { @IsString() name!: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; @Optional({ nullable: true }) @@ -103,12 +99,10 @@ export class UserAdminCreateDto { @ValidateBoolean({ optional: true }) shouldChangePassword?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) notify?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isAdmin?: boolean; } @@ -131,9 +125,7 @@ export class UserAdminUpdateDto { @IsNotEmpty() name?: string; - @Optional({ nullable: true }) - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) avatarColor?: UserAvatarColor | null; @Optional({ nullable: true }) @@ -150,8 +142,7 @@ export class UserAdminUpdateDto { @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isAdmin?: boolean; } @@ -172,7 +163,7 @@ export class UserAdminResponseDto extends UserResponseDto { quotaSizeInBytes!: number | null; @ApiProperty({ type: 'integer', format: 'int64' }) quotaUsageInBytes!: number | null; - @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) + @ValidateEnum({ enum: UserStatus, name: 'UserStatus' }) status!: string; license!: UserLicense | null; } diff --git a/server/src/validation.ts b/server/src/validation.ts index bacf4b6f5a..049b5432d6 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -31,7 +31,6 @@ import { import { CronJob } from 'cron'; import { DateTime } from 'luxon'; import sanitize from 'sanitize-filename'; -import { AssetVisibility } from 'src/enum'; import { isIP, isIPRange } from 'validator'; @Injectable() @@ -181,23 +180,9 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { return applyDecorators(...decorators); }; -type AssetVisibilityOptions = { optional?: boolean }; -export const ValidateAssetVisibility = (options?: AssetVisibilityOptions & ApiPropertyOptions) => { - const { optional, ...apiPropertyOptions } = { optional: false, ...options }; - const decorators = [ - IsEnum(AssetVisibility), - ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility, ...apiPropertyOptions }), - ]; - - if (optional) { - decorators.push(Optional()); - } - return applyDecorators(...decorators); -}; - -type BooleanOptions = { optional?: boolean }; +type BooleanOptions = { optional?: boolean; nullable?: boolean }; export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => { - const { optional, ...apiPropertyOptions } = { optional: false, ...options }; + const { optional, nullable, ...apiPropertyOptions } = options || {}; const decorators = [ ApiProperty(apiPropertyOptions), IsBoolean(), @@ -209,15 +194,37 @@ export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) = } return value; }), + optional ? Optional({ nullable }) : IsNotEmpty(), ]; - if (optional) { - decorators.push(Optional()); - } - return applyDecorators(...decorators); }; +type EnumOptions = { + enum: T; + name: string; + each?: boolean; + optional?: boolean; + nullable?: boolean; + default?: T[keyof T]; + description?: string; +}; +export const ValidateEnum = ({ + name, + enum: value, + each, + optional, + nullable, + default: defaultValue, + description, +}: EnumOptions) => { + return applyDecorators( + optional ? Optional({ nullable }) : IsNotEmpty(), + IsEnum(value, { each }), + ApiProperty({ enumName: name, enum: value, isArray: each, default: defaultValue, description }), + ); +}; + @ValidatorConstraint({ name: 'cronValidator' }) class CronValidator implements ValidatorConstraintInterface { validate(expression: string): boolean {