chore(server): move dtos (#8131)

move dtos
This commit is contained in:
Daniel Dietzler
2024-03-20 23:53:07 +01:00
committed by GitHub
parent 0c13c63bb6
commit 6d9e7694b1
145 changed files with 917 additions and 964 deletions
+78
View File
@@ -0,0 +1,78 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { UserDto, mapSimpleUser } from 'src/dtos/user.dto';
import { ActivityEntity } from 'src/entities/activity.entity';
import { Optional, ValidateUUID } from 'src/validation';
export enum ReactionType {
COMMENT = 'comment',
LIKE = 'like',
}
export enum ReactionLevel {
ALBUM = 'album',
ASSET = 'asset',
}
export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
export class ActivityResponseDto {
id!: string;
createdAt!: Date;
type!: ReactionType;
user!: UserDto;
assetId!: string | null;
comment?: string | null;
}
export class ActivityStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
comments!: number;
}
export class ActivityDto {
@ValidateUUID()
albumId!: string;
@ValidateUUID({ optional: true })
assetId?: string;
}
export class ActivitySearchDto extends ActivityDto {
@IsEnum(ReactionType)
@Optional()
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
type?: ReactionType;
@IsEnum(ReactionLevel)
@Optional()
@ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel })
level?: ReactionLevel;
@ValidateUUID({ optional: true })
userId?: string;
}
const isComment = (dto: ActivityCreateDto) => dto.type === 'comment';
export class ActivityCreateDto extends ActivityDto {
@IsEnum(ReactionType)
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
type!: ReactionType;
@ValidateIf(isComment)
@IsNotEmpty()
@IsString()
comment?: string;
}
export function mapActivity(activity: ActivityEntity): ActivityResponseDto {
return {
id: activity.id,
assetId: activity.assetId,
createdAt: activity.createdAt,
comment: activity.comment,
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
user: mapSimpleUser(activity.user),
};
}
@@ -0,0 +1,16 @@
import { mapAlbum } from 'src/dtos/album.dto';
import { albumStub } from 'test/fixtures/album.stub';
describe('mapAlbum', () => {
it('should set start and end dates', () => {
const dto = mapAlbum(albumStub.twoAssets, false);
expect(dto.startDate).toEqual(new Date('2023-02-22T05:06:29.716Z'));
expect(dto.endDate).toEqual(new Date('2023-02-23T05:06:29.716Z'));
});
it('should not set start and end dates for empty assets', () => {
const dto = mapAlbum(albumStub.empty, false);
expect(dto.startDate).toBeUndefined();
expect(dto.endDate).toBeUndefined();
});
});
+153
View File
@@ -0,0 +1,153 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class AlbumInfoDto {
@ValidateBoolean({ optional: true })
withoutAssets?: boolean;
}
export class AddUsersDto {
@ValidateUUID({ each: true })
@ArrayNotEmpty()
sharedUserIds!: string[];
}
export class CreateAlbumDto {
@IsString()
@ApiProperty()
albumName!: string;
@IsString()
@Optional()
description?: string;
@ValidateUUID({ optional: true, each: true })
sharedWithUserIds?: string[];
@ValidateUUID({ optional: true, each: true })
assetIds?: string[];
}
export class UpdateAlbumDto {
@Optional()
@IsString()
albumName?: string;
@Optional()
@IsString()
description?: string;
@ValidateUUID({ optional: true })
albumThumbnailAssetId?: string;
@ValidateBoolean({ optional: true })
isActivityEnabled?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
order?: AssetOrder;
}
export class GetAlbumsDto {
@ValidateBoolean({ optional: true })
/**
* true: only shared albums
* false: only non-shared own albums
* undefined: shared and owned albums
*/
shared?: boolean;
/**
* Only returns albums that contain the asset
* Ignores the shared parameter
* undefined: get all albums
*/
@ValidateUUID({ optional: true })
assetId?: string;
}
export class AlbumCountResponseDto {
@ApiProperty({ type: 'integer' })
owned!: number;
@ApiProperty({ type: 'integer' })
shared!: number;
@ApiProperty({ type: 'integer' })
notShared!: number;
}
export class AlbumResponseDto {
id!: string;
ownerId!: string;
albumName!: string;
description!: string;
createdAt!: Date;
updatedAt!: Date;
albumThumbnailAssetId!: string | null;
shared!: boolean;
sharedUsers!: UserResponseDto[];
hasSharedLink!: boolean;
assets!: AssetResponseDto[];
owner!: UserResponseDto;
@ApiProperty({ type: 'integer' })
assetCount!: number;
lastModifiedAssetTimestamp?: Date;
startDate?: Date;
endDate?: Date;
isActivityEnabled!: boolean;
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
}
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
const sharedUsers: UserResponseDto[] = [];
if (entity.sharedUsers) {
for (const user of entity.sharedUsers) {
sharedUsers.push(mapUser(user));
}
}
const assets = entity.assets || [];
const hasSharedLink = entity.sharedLinks?.length > 0;
const hasSharedUser = sharedUsers.length > 0;
let startDate = assets.at(0)?.fileCreatedAt || undefined;
let endDate = assets.at(-1)?.fileCreatedAt || undefined;
// Swap dates if start date is greater than end date.
if (startDate && endDate && startDate > endDate) {
[startDate, endDate] = [endDate, startDate];
}
return {
albumName: entity.albumName,
description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
id: entity.id,
ownerId: entity.ownerId,
owner: mapUser(entity.owner),
sharedUsers,
shared: hasSharedUser || hasSharedLink,
hasSharedLink,
startDate,
endDate,
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled,
order: entity.order,
};
};
export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true);
export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false);
+26
View File
@@ -0,0 +1,26 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { Optional } from 'src/validation';
export class APIKeyCreateDto {
@IsString()
@IsNotEmpty()
@Optional()
name?: string;
}
export class APIKeyUpdateDto {
@IsString()
@IsNotEmpty()
name!: string;
}
export class APIKeyCreateResponseDto {
secret!: string;
apiKey!: APIKeyResponseDto;
}
export class APIKeyResponseDto {
id!: string;
name!: string;
createdAt!: Date;
updatedAt!: Date;
}
+33
View File
@@ -0,0 +1,33 @@
import { ValidateUUID } from 'src/validation';
/** @deprecated Use `BulkIdResponseDto` instead */
export enum AssetIdErrorReason {
DUPLICATE = 'duplicate',
NO_PERMISSION = 'no_permission',
NOT_FOUND = 'not_found',
}
/** @deprecated Use `BulkIdResponseDto` instead */
export class AssetIdsResponseDto {
assetId!: string;
success!: boolean;
error?: AssetIdErrorReason;
}
export enum BulkIdErrorReason {
DUPLICATE = 'duplicate',
NO_PERMISSION = 'no_permission',
NOT_FOUND = 'not_found',
UNKNOWN = 'unknown',
}
export class BulkIdsDto {
@ValidateUUID({ each: true })
ids!: string[];
}
export class BulkIdResponseDto {
id!: string;
success!: boolean;
error?: BulkIdErrorReason;
}
+148
View File
@@ -0,0 +1,148 @@
import { ApiProperty } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto';
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
export class SanitizedAssetResponseDto {
id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType;
thumbhash!: string | null;
resized!: boolean;
localDateTime!: Date;
duration!: string;
livePhotoVideoId?: string | null;
hasMetadata!: boolean;
}
export class AssetResponseDto extends SanitizedAssetResponseDto {
deviceAssetId!: string;
deviceId!: string;
ownerId!: string;
owner?: UserResponseDto;
libraryId!: string;
originalPath!: string;
originalFileName!: string;
fileCreatedAt!: Date;
fileModifiedAt!: Date;
updatedAt!: Date;
isFavorite!: boolean;
isArchived!: boolean;
isTrashed!: boolean;
isOffline!: boolean;
isExternal!: boolean;
isReadOnly!: boolean;
exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto;
tags?: TagResponseDto[];
people?: PersonWithFacesResponseDto[];
/**base64 encoded sha1 hash */
checksum!: string;
stackParentId?: string | null;
stack?: AssetResponseDto[];
@ApiProperty({ type: 'integer' })
stackCount!: number | null;
}
export type AssetMapOptions = {
stripMetadata?: boolean;
withStack?: boolean;
auth?: AuthDto;
};
const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
const result: PersonWithFacesResponseDto[] = [];
if (faces) {
for (const face of faces) {
if (face.person) {
const existingPersonEntry = result.find((item) => item.id === face.person!.id);
if (existingPersonEntry) {
existingPersonEntry.faces.push(face);
} else {
result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] });
}
}
}
}
return result;
};
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options;
if (stripMetadata) {
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id,
type: entity.type,
thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime,
resized: !!entity.resizePath,
duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false,
};
return sanitizedAssetResponse as AssetResponseDto;
}
return {
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined,
deviceId: entity.deviceId,
libraryId: entity.libraryId,
type: entity.type,
originalPath: entity.originalPath,
originalFileName: entity.originalFileName,
resized: !!entity.resizePath,
thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
localDateTime: entity.localDateTime,
updatedAt: entity.updatedAt,
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
isArchived: options.auth?.user.id === entity.ownerId ? entity.isArchived : false,
isTrashed: !!entity.deletedAt,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag),
people: peopleWithFaces(entity.faces),
checksum: entity.checksum.toString('base64'),
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
stack: withStack
? entity.stack?.assets
.filter((a) => a.id !== entity.stack?.primaryAssetId)
.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
: undefined,
stackCount: entity.stack?.assets?.length ?? null,
isExternal: entity.isExternal,
isOffline: entity.isOffline,
isReadOnly: entity.isReadOnly,
hasMetadata: true,
};
}
export class MemoryLaneResponseDto {
title!: string;
assets!: AssetResponseDto[];
}
export class SmartInfoResponseDto {
tags?: string[] | null;
objects?: string[] | null;
}
export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
return {
tags: entity.tags,
objects: entity.objects,
};
}
+132
View File
@@ -0,0 +1,132 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsDateString,
IsEnum,
IsInt,
IsLatitude,
IsLongitude,
IsNotEmpty,
IsPositive,
IsString,
ValidateIf,
} from 'class-validator';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AssetType } from 'src/entities/asset.entity';
import { AssetStats } from 'src/interfaces/asset.repository';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class DeviceIdDto {
@IsNotEmpty()
@IsString()
deviceId!: string;
}
const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
o.latitude !== undefined || o.longitude !== undefined;
const ValidateGPS = () => ValidateIf(hasGPS);
export class UpdateAssetBase {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@Optional()
@IsDateString()
dateTimeOriginal?: string;
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
latitude?: number;
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
longitude?: number;
}
export class AssetBulkUpdateDto extends UpdateAssetBase {
@ValidateUUID({ each: true })
ids!: string[];
@ValidateUUID({ optional: true })
stackParentId?: string;
@ValidateBoolean({ optional: true })
removeParent?: boolean;
}
export class UpdateAssetDto extends UpdateAssetBase {
@Optional()
@IsString()
description?: string;
}
export class RandomAssetsDto {
@Optional()
@IsInt()
@IsPositive()
@Type(() => Number)
count?: number;
}
export class AssetBulkDeleteDto extends BulkIdsDto {
@ValidateBoolean({ optional: true })
force?: boolean;
}
export class AssetIdsDto {
@ValidateUUID({ each: true })
assetIds!: string[];
}
export enum AssetJobName {
REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
REFRESH_METADATA = 'refresh-metadata',
TRANSCODE_VIDEO = 'transcode-video',
}
export class AssetJobsDto extends AssetIdsDto {
@ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
@IsEnum(AssetJobName)
name!: AssetJobName;
}
export class AssetStatsDto {
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isTrashed?: boolean;
}
export class AssetStatsResponseDto {
@ApiProperty({ type: 'integer' })
images!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer' })
total!: number;
}
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
return {
images: stats[AssetType.IMAGE],
videos: stats[AssetType.VIDEO],
total: Object.values(stats).reduce((total, value) => total + value, 0),
};
};
export enum UploadFieldName {
ASSET_DATA = 'assetData',
LIVE_PHOTO_DATA = 'livePhotoData',
SIDECAR_DATA = 'sidecarData',
PROFILE_DATA = 'file',
}
+74
View File
@@ -0,0 +1,74 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
import { EntityType } from 'src/entities/audit.entity';
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
export class AuditDeletesDto {
@ValidateDate()
after!: Date;
@ApiProperty({ enum: EntityType, enumName: 'EntityType' })
@IsEnum(EntityType)
entityType!: EntityType;
@Optional()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })
userId?: string;
}
export enum PathEntityType {
ASSET = 'asset',
PERSON = 'person',
USER = 'user',
}
export class AuditDeletesResponseDto {
needsFullSync!: boolean;
ids!: string[];
}
export class FileReportDto {
orphans!: FileReportItemDto[];
extras!: string[];
}
export class FileChecksumDto {
@IsString({ each: true })
filenames!: string[];
}
export class FileChecksumResponseDto {
filename!: string;
checksum!: string;
}
export class FileReportFixDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => FileReportItemDto)
items!: FileReportItemDto[];
}
// used both as request and response dto
export class FileReportItemDto {
@ValidateUUID()
entityId!: string;
@ApiProperty({ enumName: 'PathEntityType', enum: PathEntityType })
@IsEnum(PathEntityType)
entityType!: PathEntityType;
@ApiProperty({ enumName: 'PathType', enum: PathEnum })
@IsEnum(PathEnum)
pathType!: PathType;
@IsString()
pathValue!: string;
checksum?: string;
}
+114
View File
@@ -0,0 +1,114 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
import { APIKeyEntity } from 'src/entities/api-key.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { UserEntity } from 'src/entities/user.entity';
export class AuthDto {
user!: UserEntity;
apiKey?: APIKeyEntity;
sharedLink?: SharedLinkEntity;
userToken?: UserTokenEntity;
}
export class LoginCredentialDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
}
export class LoginResponseDto {
accessToken!: string;
userId!: string;
userEmail!: string;
name!: string;
profileImagePath!: string;
isAdmin!: boolean;
shouldChangePassword!: boolean;
}
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
return {
accessToken: accessToken,
userId: entity.id,
userEmail: entity.email,
name: entity.name,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
};
}
export class LogoutResponseDto {
successful!: boolean;
redirectUri!: string;
}
export class SignUpDto extends LoginCredentialDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Admin' })
name!: string;
}
export class ChangePasswordDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsString()
@IsNotEmpty()
@MinLength(8)
@ApiProperty({ example: 'password' })
newPassword!: string;
}
export class ValidateAccessTokenResponseDto {
authStatus!: boolean;
}
export class AuthDeviceResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});
export class OAuthCallbackDto {
@IsNotEmpty()
@IsString()
@ApiProperty()
url!: string;
}
export class OAuthConfigDto {
@IsNotEmpty()
@IsString()
redirectUri!: string;
}
export class OAuthAuthorizeResponseDto {
url!: string;
}
+32
View File
@@ -0,0 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsPositive } from 'class-validator';
import { Optional, ValidateUUID } from 'src/validation';
export class DownloadInfoDto {
@ValidateUUID({ each: true, optional: true })
assetIds?: string[];
@ValidateUUID({ optional: true })
albumId?: string;
@ValidateUUID({ optional: true })
userId?: string;
@IsInt()
@IsPositive()
@Optional()
@ApiProperty({ type: 'integer' })
archiveSize?: number;
}
export class DownloadResponseDto {
@ApiProperty({ type: 'integer' })
totalSize!: number;
archives!: DownloadArchiveInfo[];
}
export class DownloadArchiveInfo {
@ApiProperty({ type: 'integer' })
size!: number;
assetIds!: string[];
}
+66
View File
@@ -0,0 +1,66 @@
import { ApiProperty } from '@nestjs/swagger';
import { ExifEntity } from 'src/entities/exif.entity';
export class ExifResponseDto {
make?: string | null = null;
model?: string | null = null;
exifImageWidth?: number | null = null;
exifImageHeight?: number | null = null;
@ApiProperty({ type: 'integer', format: 'int64' })
fileSizeInByte?: number | null = null;
orientation?: string | null = null;
dateTimeOriginal?: Date | null = null;
modifyDate?: Date | null = null;
timeZone?: string | null = null;
lensModel?: string | null = null;
fNumber?: number | null = null;
focalLength?: number | null = null;
iso?: number | null = null;
exposureTime?: string | null = null;
latitude?: number | null = null;
longitude?: number | null = null;
city?: string | null = null;
state?: string | null = null;
country?: string | null = null;
description?: string | null = null;
projectionType?: string | null = null;
}
export function mapExif(entity: ExifEntity): ExifResponseDto {
return {
make: entity.make,
model: entity.model,
exifImageWidth: entity.exifImageWidth,
exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation,
dateTimeOriginal: entity.dateTimeOriginal,
modifyDate: entity.modifyDate,
timeZone: entity.timeZone,
lensModel: entity.lensModel,
fNumber: entity.fNumber,
focalLength: entity.focalLength,
iso: entity.iso,
exposureTime: entity.exposureTime,
latitude: entity.latitude,
longitude: entity.longitude,
city: entity.city,
state: entity.state,
country: entity.country,
description: entity.description,
projectionType: entity.projectionType,
};
}
export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
return {
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation,
dateTimeOriginal: entity.dateTimeOriginal,
timeZone: entity.timeZone,
projectionType: entity.projectionType,
exifImageWidth: entity.exifImageWidth,
exifImageHeight: entity.exifImageHeight,
};
}
+87
View File
@@ -0,0 +1,87 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { JobCommand, QueueName } from 'src/domain/job/job.constants';
import { ValidateBoolean } from 'src/validation';
export class JobIdParamDto {
@IsNotEmpty()
@IsEnum(QueueName)
@ApiProperty({ type: String, enum: QueueName, enumName: 'JobName' })
id!: QueueName;
}
export class JobCommandDto {
@IsNotEmpty()
@IsEnum(JobCommand)
@ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' })
command!: JobCommand;
@ValidateBoolean({ optional: true })
force!: boolean;
}
export class JobCountsDto {
@ApiProperty({ type: 'integer' })
active!: number;
@ApiProperty({ type: 'integer' })
completed!: number;
@ApiProperty({ type: 'integer' })
failed!: number;
@ApiProperty({ type: 'integer' })
delayed!: number;
@ApiProperty({ type: 'integer' })
waiting!: number;
@ApiProperty({ type: 'integer' })
paused!: number;
}
export class QueueStatusDto {
isActive!: boolean;
isPaused!: boolean;
}
export class JobStatusDto {
@ApiProperty({ type: JobCountsDto })
jobCounts!: JobCountsDto;
@ApiProperty({ type: QueueStatusDto })
queueStatus!: QueueStatusDto;
}
export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto> {
@ApiProperty({ type: JobStatusDto })
[QueueName.THUMBNAIL_GENERATION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.METADATA_EXTRACTION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.VIDEO_CONVERSION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.SMART_SEARCH]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.MIGRATION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.BACKGROUND_TASK]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.SEARCH]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.FACE_DETECTION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.FACIAL_RECOGNITION]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.SIDECAR]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.LIBRARY]!: JobStatusDto;
}
+167
View File
@@ -0,0 +1,167 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class CreateLibraryDto {
@IsEnum(LibraryType)
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
type!: LibraryType;
@ValidateUUID()
ownerId!: string;
@IsString()
@Optional()
@IsNotEmpty()
name?: string;
@ValidateBoolean({ optional: true })
isVisible?: boolean;
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
exclusionPatterns?: string[];
@ValidateBoolean({ optional: true })
isWatched?: boolean;
}
export class UpdateLibraryDto {
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ValidateBoolean({ optional: true })
isVisible?: boolean;
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
@Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
exclusionPatterns?: string[];
}
export class CrawlOptionsDto {
pathsToCrawl!: string[];
includeHidden? = false;
exclusionPatterns?: string[];
}
export class ValidateLibraryDto {
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
importPaths?: string[];
@Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@ArrayUnique()
@ArrayMaxSize(128)
exclusionPatterns?: string[];
}
export class ValidateLibraryResponseDto {
importPaths?: ValidateLibraryImportPathResponseDto[];
}
export class ValidateLibraryImportPathResponseDto {
importPath!: string;
isValid: boolean = false;
message?: string;
}
export class LibrarySearchDto {
@ValidateUUID({ optional: true })
userId?: string;
}
export class ScanLibraryDto {
@ValidateBoolean({ optional: true })
refreshModifiedFiles?: boolean;
@ValidateBoolean({ optional: true })
refreshAllFiles?: boolean;
}
export class SearchLibraryDto {
@IsEnum(LibraryType)
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
@Optional()
type?: LibraryType;
}
export class LibraryResponseDto {
id!: string;
ownerId!: string;
name!: string;
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
type!: LibraryType;
@ApiProperty({ type: 'integer' })
assetCount!: number;
importPaths!: string[];
exclusionPatterns!: string[];
createdAt!: Date;
updatedAt!: Date;
refreshedAt!: Date | null;
}
export class LibraryStatsResponseDto {
@ApiProperty({ type: 'integer' })
photos = 0;
@ApiProperty({ type: 'integer' })
videos = 0;
@ApiProperty({ type: 'integer' })
total = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usage = 0;
}
export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
let assetCount = 0;
if (entity.assets) {
assetCount = entity.assets.length;
}
return {
id: entity.id,
ownerId: entity.ownerId,
type: entity.type,
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
refreshedAt: entity.refreshedAt,
assetCount,
importPaths: entity.importPaths,
exclusionPatterns: entity.exclusionPatterns,
};
}
+48
View File
@@ -0,0 +1,48 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.repository';
import { Optional, ValidateBoolean } from 'src/validation';
export class ModelConfig {
@ValidateBoolean()
enabled!: boolean;
@IsString()
@IsNotEmpty()
modelName!: string;
@IsEnum(ModelType)
@Optional()
@ApiProperty({ enumName: 'ModelType', enum: ModelType })
modelType?: ModelType;
}
export class CLIPConfig extends ModelConfig {
@IsEnum(CLIPMode)
@Optional()
@ApiProperty({ enumName: 'CLIPMode', enum: CLIPMode })
mode?: CLIPMode;
}
export class RecognitionConfig extends ModelConfig {
@IsNumber()
@Min(0)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'float' })
minScore!: number;
@IsNumber()
@Min(0)
@Max(2)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'float' })
maxDistance!: number;
@IsNumber()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
minFaces!: number;
}
+11
View File
@@ -0,0 +1,11 @@
import { IsNotEmpty } from 'class-validator';
import { UserResponseDto } from 'src/dtos/user.dto';
export class UpdatePartnerDto {
@IsNotEmpty()
inTimeline!: boolean;
}
export class PartnerResponseDto extends UserResponseDto {
inTimeline?: boolean;
}
+161
View File
@@ -0,0 +1,161 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
export class PersonCreateDto {
/**
* Person name.
*/
@Optional()
@IsString()
name?: string;
/**
* Person date of birth.
* Note: the mobile app cannot currently set the birth date to null.
*/
@MaxDate(() => new Date(), { message: 'Birth date cannot be in the future' })
@ValidateDate({ optional: true, nullable: true, format: 'date' })
birthDate?: Date | null;
/**
* Person visibility
*/
@ValidateBoolean({ optional: true })
isHidden?: boolean;
}
export class PersonUpdateDto extends PersonCreateDto {
/**
* Asset is used to get the feature face thumbnail.
*/
@Optional()
@IsString()
featureFaceAssetId?: string;
}
export class PeopleUpdateDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => PeopleUpdateItem)
people!: PeopleUpdateItem[];
}
export class PeopleUpdateItem extends PersonUpdateDto {
/**
* Person id.
*/
@IsString()
@IsNotEmpty()
id!: string;
}
export class MergePersonDto {
@ValidateUUID({ each: true })
ids!: string[];
}
export class PersonSearchDto {
@ValidateBoolean({ optional: true })
withHidden?: boolean;
}
export class PersonResponseDto {
id!: string;
name!: string;
@ApiProperty({ format: 'date' })
birthDate!: Date | null;
thumbnailPath!: string;
isHidden!: boolean;
}
export class PersonWithFacesResponseDto extends PersonResponseDto {
faces!: AssetFaceWithoutPersonResponseDto[];
}
export class AssetFaceWithoutPersonResponseDto {
@ValidateUUID()
id!: string;
@ApiProperty({ type: 'integer' })
imageHeight!: number;
@ApiProperty({ type: 'integer' })
imageWidth!: number;
@ApiProperty({ type: 'integer' })
boundingBoxX1!: number;
@ApiProperty({ type: 'integer' })
boundingBoxX2!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY1!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY2!: number;
}
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
person!: PersonResponseDto | null;
}
export class AssetFaceUpdateDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetFaceUpdateItem)
data!: AssetFaceUpdateItem[];
}
export class FaceDto {
@ValidateUUID()
id!: string;
}
export class AssetFaceUpdateItem {
@ValidateUUID()
personId!: string;
@ValidateUUID()
assetId!: string;
}
export class PersonStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
assets!: number;
}
export class PeopleResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;
@ApiProperty({ type: 'integer' })
hidden!: number;
people!: PersonResponseDto[];
}
export function mapPerson(person: PersonEntity): PersonResponseDto {
return {
id: person.id,
name: person.name,
birthDate: person.birthDate,
thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden,
};
}
export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPersonResponseDto {
return {
id: face.id,
imageHeight: face.imageHeight,
imageWidth: face.imageWidth,
boundingBoxX1: face.boundingBoxX1,
boundingBoxX2: face.boundingBoxX2,
boundingBoxY1: face.boundingBoxY1,
boundingBoxY2: face.boundingBoxY2,
};
}
export function mapFaces(face: AssetFaceEntity, auth: AuthDto): AssetFaceResponseDto {
return {
...mapFacesWithoutPerson(face),
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
};
}
+395
View File
@@ -0,0 +1,395 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder } from 'src/entities/album.entity';
import { AssetType } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
class BaseSearchDto {
@ValidateUUID({ optional: true })
libraryId?: string;
@IsString()
@IsNotEmpty()
@Optional()
deviceId?: string;
@IsEnum(AssetType)
@Optional()
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
@ApiProperty({ default: false })
withArchived?: boolean;
@ValidateBoolean({ optional: true })
isEncoded?: boolean;
@ValidateBoolean({ optional: true })
isExternal?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isMotion?: boolean;
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateBoolean({ optional: true })
isReadOnly?: boolean;
@ValidateBoolean({ optional: true })
isVisible?: boolean;
@ValidateBoolean({ optional: true })
withDeleted?: boolean;
@ValidateBoolean({ optional: true })
withExif?: boolean;
@ValidateDate({ optional: true })
createdBefore?: Date;
@ValidateDate({ optional: true })
createdAfter?: Date;
@ValidateDate({ optional: true })
updatedBefore?: Date;
@ValidateDate({ optional: true })
updatedAfter?: Date;
@ValidateDate({ optional: true })
trashedBefore?: Date;
@ValidateDate({ optional: true })
trashedAfter?: Date;
@ValidateDate({ optional: true })
takenBefore?: Date;
@ValidateDate({ optional: true })
takenAfter?: Date;
@IsString()
@IsNotEmpty()
@Optional()
city?: string;
@IsString()
@IsNotEmpty()
@Optional()
state?: string;
@IsString()
@IsNotEmpty()
@Optional()
country?: string;
@IsString()
@IsNotEmpty()
@Optional()
make?: string;
@IsString()
@IsNotEmpty()
@Optional()
model?: string;
@IsString()
@IsNotEmpty()
@Optional()
lensModel?: string;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
@ValidateBoolean({ optional: true })
isNotInAlbum?: boolean;
@ValidateUUID({ each: true, optional: true })
personIds?: string[];
}
export class MetadataSearchDto extends BaseSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@IsString()
@IsNotEmpty()
@Optional()
deviceAssetId?: string;
@IsString()
@IsNotEmpty()
@Optional()
checksum?: string;
@ValidateBoolean({ optional: true })
withStacked?: boolean;
@ValidateBoolean({ optional: true })
withPeople?: boolean;
@IsString()
@IsNotEmpty()
@Optional()
originalFileName?: string;
@IsString()
@IsNotEmpty()
@Optional()
originalPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
resizePath?: string;
@IsString()
@IsNotEmpty()
@Optional()
webpPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
encodedVideoPath?: string;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
}
export class SmartSearchDto extends BaseSearchDto {
@IsString()
@IsNotEmpty()
query!: string;
}
// TODO: remove after implementing new search filters
/** @deprecated */
export class SearchDto {
@IsString()
@IsNotEmpty()
@Optional()
q?: string;
@IsString()
@IsNotEmpty()
@Optional()
query?: string;
@ValidateBoolean({ optional: true })
smart?: boolean;
/** @deprecated */
@ValidateBoolean({ optional: true })
clip?: boolean;
@IsEnum(AssetType)
@Optional()
type?: AssetType;
@ValidateBoolean({ optional: true })
recent?: boolean;
@ValidateBoolean({ optional: true })
motion?: boolean;
@ValidateBoolean({ optional: true })
withArchived?: boolean;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
}
export class SearchPlacesDto {
@IsString()
@IsNotEmpty()
name!: string;
}
export class SearchPeopleDto {
@IsString()
@IsNotEmpty()
name!: string;
@ValidateBoolean({ optional: true })
withHidden?: boolean;
}
export class PlacesResponseDto {
name!: string;
latitude!: number;
longitude!: number;
admin1name?: string;
admin2name?: string;
}
export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto {
return {
name: place.name,
latitude: place.latitude,
longitude: place.longitude,
admin1name: place.admin1Name,
admin2name: place.admin2Name,
};
}
export enum SearchSuggestionType {
COUNTRY = 'country',
STATE = 'state',
CITY = 'city',
CAMERA_MAKE = 'camera-make',
CAMERA_MODEL = 'camera-model',
}
export class SearchSuggestionRequestDto {
@IsEnum(SearchSuggestionType)
@IsNotEmpty()
@ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
type!: SearchSuggestionType;
@IsString()
@Optional()
country?: string;
@IsString()
@Optional()
state?: string;
@IsString()
@Optional()
make?: string;
@IsString()
@Optional()
model?: string;
}
class SearchFacetCountResponseDto {
@ApiProperty({ type: 'integer' })
count!: number;
value!: string;
}
class SearchFacetResponseDto {
fieldName!: string;
counts!: SearchFacetCountResponseDto[];
}
class SearchAlbumResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;
@ApiProperty({ type: 'integer' })
count!: number;
items!: AlbumResponseDto[];
facets!: SearchFacetResponseDto[];
}
class SearchAssetResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;
@ApiProperty({ type: 'integer' })
count!: number;
items!: AssetResponseDto[];
facets!: SearchFacetResponseDto[];
nextPage!: string | null;
}
export class SearchResponseDto {
albums!: SearchAlbumResponseDto;
assets!: SearchAssetResponseDto;
}
class SearchExploreItem {
value!: string;
data!: AssetResponseDto;
}
export class SearchExploreResponseDto {
fieldName!: string;
items!: SearchExploreItem[];
}
export class MapMarkerDto {
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateDate({ optional: true })
fileCreatedAfter?: Date;
@ValidateDate({ optional: true })
fileCreatedBefore?: Date;
@ValidateBoolean({ optional: true })
withPartners?: boolean;
}
export class MemoryLaneDto {
@IsInt()
@Type(() => Number)
@Max(31)
@Min(1)
@ApiProperty({ type: 'integer' })
day!: number;
@IsInt()
@Type(() => Number)
@Max(12)
@Min(1)
@ApiProperty({ type: 'integer' })
month!: number;
}
export class MapMarkerResponseDto {
@ApiProperty()
id!: string;
@ApiProperty({ format: 'double' })
lat!: number;
@ApiProperty({ format: 'double' })
lon!: number;
@ApiProperty()
city!: string | null;
@ApiProperty()
state!: string | null;
@ApiProperty()
country!: string | null;
}
+118
View File
@@ -0,0 +1,118 @@
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import type { DateTime } from 'luxon';
import { FeatureFlags } from 'src/cores/system-config.core';
import { IVersion, VersionType } from 'src/domain/domain.constant';
import { SystemConfigThemeDto } from 'src/dtos/system-config-theme.dto';
export class ServerPingResponse {
@ApiResponseProperty({ type: String, example: 'pong' })
res!: string;
}
export class ServerInfoResponseDto {
diskSize!: string;
diskUse!: string;
diskAvailable!: string;
@ApiProperty({ type: 'integer', format: 'int64' })
diskSizeRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskUseRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskAvailableRaw!: number;
@ApiProperty({ type: 'number', format: 'float' })
diskUsagePercentage!: number;
}
export class ServerVersionResponseDto implements IVersion {
@ApiProperty({ type: 'integer' })
major!: number;
@ApiProperty({ type: 'integer' })
minor!: number;
@ApiProperty({ type: 'integer' })
patch!: number;
}
export class UsageByUserDto {
@ApiProperty({ type: 'string' })
userId!: string;
@ApiProperty({ type: 'string' })
userName!: string;
@ApiProperty({ type: 'integer' })
photos!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
usage!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes!: number | null;
}
export class ServerStatsResponseDto {
@ApiProperty({ type: 'integer' })
photos = 0;
@ApiProperty({ type: 'integer' })
videos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usage = 0;
@ApiProperty({
isArray: true,
type: UsageByUserDto,
title: 'Array of usage for each user',
example: [
{
photos: 1,
videos: 1,
diskUsageRaw: 1,
},
],
})
usageByUser: UsageByUserDto[] = [];
}
export class ServerMediaTypesResponseDto {
video!: string[];
image!: string[];
sidecar!: string[];
}
export class ServerThemeDto extends SystemConfigThemeDto {}
export class ServerConfigDto {
oauthButtonText!: string;
loginPageMessage!: string;
@ApiProperty({ type: 'integer' })
trashDays!: number;
@ApiProperty({ type: 'integer' })
userDeleteDelay!: number;
isInitialized!: boolean;
isOnboarded!: boolean;
externalDomain!: string;
}
export class ServerFeaturesDto implements FeatureFlags {
smartSearch!: boolean;
configFile!: boolean;
facialRecognition!: boolean;
map!: boolean;
trash!: boolean;
reverseGeocoding!: boolean;
oauth!: boolean;
oauthAutoLaunch!: boolean;
passwordLogin!: boolean;
sidecar!: boolean;
search!: boolean;
}
export interface ReleaseNotification {
isAvailable: VersionType;
checkedAt: DateTime<boolean> | null;
serverVersion: ServerVersionResponseDto;
releaseVersion: ServerVersionResponseDto;
}
+143
View File
@@ -0,0 +1,143 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import _ from 'lodash';
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
export class SharedLinkCreateDto {
@IsEnum(SharedLinkType)
@ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
type!: SharedLinkType;
@ValidateUUID({ each: true, optional: true })
assetIds?: string[];
@ValidateUUID({ optional: true })
albumId?: string;
@IsString()
@Optional()
description?: string;
@IsString()
@Optional()
password?: string;
@ValidateDate({ optional: true, nullable: true })
expiresAt?: Date | null = null;
@ValidateBoolean({ optional: true })
allowUpload?: boolean;
@ValidateBoolean({ optional: true })
allowDownload?: boolean = true;
@ValidateBoolean({ optional: true })
showMetadata?: boolean = true;
}
export class SharedLinkEditDto {
@Optional()
description?: string;
@Optional()
password?: string;
@Optional({ nullable: true })
expiresAt?: Date | null;
@Optional()
allowUpload?: boolean;
@ValidateBoolean({ optional: true })
allowDownload?: boolean;
@ValidateBoolean({ optional: true })
showMetadata?: boolean;
/**
* Few clients cannot send null to set the expiryTime to never.
* Setting this flag and not sending expiryAt is considered as null instead.
* Clients that can send null values can ignore this.
*/
@ValidateBoolean({ optional: true })
changeExpiryTime?: boolean;
}
export class SharedLinkPasswordDto {
@IsString()
@Optional()
@ApiProperty({ example: 'password' })
password?: string;
@IsString()
@Optional()
token?: string;
}
export class SharedLinkResponseDto {
id!: string;
description!: string | null;
password!: string | null;
token?: string | null;
userId!: string;
key!: string;
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
type!: SharedLinkType;
createdAt!: Date;
expiresAt!: Date | null;
assets!: AssetResponseDto[];
album?: AlbumResponseDto;
allowUpload!: boolean;
allowDownload!: boolean;
showMetadata!: boolean;
}
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
return {
id: sharedLink.id,
description: sharedLink.description,
password: sharedLink.password,
userId: sharedLink.userId,
key: sharedLink.key.toString('base64url'),
type: sharedLink.type,
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: assets.map((asset) => mapAsset(asset)),
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif,
};
}
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
return {
id: sharedLink.id,
description: sharedLink.description,
password: sharedLink.password,
userId: sharedLink.userId,
key: sharedLink.key.toString('base64url'),
type: sharedLink.type,
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[],
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif,
};
}
+9
View File
@@ -0,0 +1,9 @@
import { ValidateUUID } from 'src/validation';
export class UpdateStackParentDto {
@ValidateUUID()
oldParentId!: string;
@ValidateUUID()
newParentId!: string;
}
+103
View File
@@ -0,0 +1,103 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator';
import {
AudioCodec,
CQMode,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
} from 'src/entities/system-config.entity';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigFFmpegDto {
@IsInt()
@Min(0)
@Max(51)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
crf!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
threads!: number;
@IsString()
preset!: string;
@IsEnum(VideoCodec)
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
targetVideoCodec!: VideoCodec;
@IsEnum(VideoCodec, { each: true })
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
acceptedVideoCodecs!: VideoCodec[];
@IsEnum(AudioCodec)
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
targetAudioCodec!: AudioCodec;
@IsEnum(AudioCodec, { each: true })
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
acceptedAudioCodecs!: AudioCodec[];
@IsString()
targetResolution!: string;
@IsString()
maxBitrate!: string;
@IsInt()
@Min(-1)
@Max(16)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
bframes!: number;
@IsInt()
@Min(0)
@Max(6)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
refs!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
gopSize!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
npl!: number;
@ValidateBoolean()
temporalAQ!: boolean;
@IsEnum(CQMode)
@ApiProperty({ enumName: 'CQMode', enum: CQMode })
cqMode!: CQMode;
@ValidateBoolean()
twoPass!: boolean;
@IsString()
preferredHwDevice!: string;
@IsEnum(TranscodePolicy)
@ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
transcode!: TranscodePolicy;
@IsEnum(TranscodeHWAccel)
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
accel!: TranscodeHWAccel;
@IsEnum(ToneMapping)
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
tonemap!: ToneMapping;
}
+73
View File
@@ -0,0 +1,73 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
import { ConcurrentQueueName, QueueName } from 'src/domain/job/job.constants';
export class JobSettingsDto {
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
concurrency!: number;
}
export class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto> {
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.METADATA_EXTRACTION]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.VIDEO_CONVERSION]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.SMART_SEARCH]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.MIGRATION]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.BACKGROUND_TASK]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.SEARCH]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.FACE_DETECTION]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.SIDECAR]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.LIBRARY]!: JobSettingsDto;
}
@@ -0,0 +1,49 @@
import { Type } from 'class-transformer';
import {
IsNotEmpty,
IsObject,
IsString,
Validate,
ValidateIf,
ValidateNested,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { ValidateBoolean, validateCronExpression } from 'src/validation';
const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
@ValidatorConstraint({ name: 'cronValidator' })
class CronValidator implements ValidatorConstraintInterface {
validate(expression: string): boolean {
return validateCronExpression(expression);
}
}
export class SystemConfigLibraryScanDto {
@ValidateBoolean()
enabled!: boolean;
@ValidateIf(isEnabled)
@IsNotEmpty()
@Validate(CronValidator, { message: 'Invalid cron expression' })
@IsString()
cronExpression!: string;
}
export class SystemConfigLibraryWatchDto {
@ValidateBoolean()
enabled!: boolean;
}
export class SystemConfigLibraryDto {
@Type(() => SystemConfigLibraryScanDto)
@ValidateNested()
@IsObject()
scan!: SystemConfigLibraryScanDto;
@Type(() => SystemConfigLibraryWatchDto)
@ValidateNested()
@IsObject()
watch!: SystemConfigLibraryWatchDto;
}
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';
import { LogLevel } from 'src/entities/system-config.entity';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigLoggingDto {
@ValidateBoolean()
enabled!: boolean;
@ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })
@IsEnum(LogLevel)
level!: LogLevel;
}
@@ -0,0 +1,23 @@
import { Type } from 'class-transformer';
import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator';
import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigMachineLearningDto {
@ValidateBoolean()
enabled!: boolean;
@IsUrl({ require_tld: false, allow_underscores: true })
@ValidateIf((dto) => dto.enabled)
url!: string;
@Type(() => CLIPConfig)
@ValidateNested()
@IsObject()
clip!: CLIPConfig;
@Type(() => RecognitionConfig)
@ValidateNested()
@IsObject()
facialRecognition!: RecognitionConfig;
}
+13
View File
@@ -0,0 +1,13 @@
import { IsString } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigMapDto {
@ValidateBoolean()
enabled!: boolean;
@IsString()
lightStyle!: string;
@IsString()
darkStyle!: string;
}
@@ -0,0 +1,6 @@
import { ValidateBoolean } from 'src/validation';
export class SystemConfigNewVersionCheckDto {
@ValidateBoolean()
enabled!: boolean;
}
@@ -0,0 +1,58 @@
import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
export class SystemConfigOAuthDto {
@ValidateBoolean()
autoLaunch!: boolean;
@ValidateBoolean()
autoRegister!: boolean;
@IsString()
buttonText!: string;
@ValidateIf(isEnabled)
@IsNotEmpty()
@IsString()
clientId!: string;
@ValidateIf(isEnabled)
@IsNotEmpty()
@IsString()
clientSecret!: string;
@IsNumber()
@Min(0)
defaultStorageQuota!: number;
@ValidateBoolean()
enabled!: boolean;
@ValidateIf(isEnabled)
@IsNotEmpty()
@IsString()
issuerUrl!: string;
@ValidateBoolean()
mobileOverrideEnabled!: boolean;
@ValidateIf(isOverrideEnabled)
@IsUrl()
mobileRedirectUri!: string;
@IsString()
scope!: string;
@IsString()
@IsNotEmpty()
signingAlgorithm!: string;
@IsString()
storageLabelClaim!: string;
@IsString()
storageQuotaClaim!: string;
}
@@ -0,0 +1,6 @@
import { ValidateBoolean } from 'src/validation';
export class SystemConfigPasswordLoginDto {
@ValidateBoolean()
enabled!: boolean;
}
@@ -0,0 +1,6 @@
import { ValidateBoolean } from 'src/validation';
export class SystemConfigReverseGeocodingDto {
@ValidateBoolean()
enabled!: boolean;
}
@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class SystemConfigServerDto {
@IsString()
externalDomain!: string;
@IsString()
loginPageMessage!: string;
}
@@ -0,0 +1,25 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigStorageTemplateDto {
@ValidateBoolean()
enabled!: boolean;
@ValidateBoolean()
hashVerificationEnabled!: boolean;
@IsNotEmpty()
@IsString()
template!: string;
}
export class SystemConfigTemplateStorageOptionDto {
yearOptions!: string[];
monthOptions!: string[];
weekOptions!: string[];
dayOptions!: string[];
hourOptions!: string[];
minuteOptions!: string[];
secondOptions!: string[];
presetOptions!: string[];
}
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class SystemConfigThemeDto {
@IsString()
customCss!: string;
}
@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, Max, Min } from 'class-validator';
import { Colorspace } from 'src/entities/system-config.entity';
export class SystemConfigThumbnailDto {
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
webpSize!: number;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
jpegSize!: number;
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
quality!: number;
@IsEnum(Colorspace)
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
colorspace!: Colorspace;
}
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, Min } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class SystemConfigTrashDto {
@ValidateBoolean()
enabled!: boolean;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
days!: number;
}
+11
View File
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, Min } from 'class-validator';
export class SystemConfigUserDto {
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
deleteDelay!: number;
}
+105
View File
@@ -0,0 +1,105 @@
import { Type } from 'class-transformer';
import { IsObject, ValidateNested } from 'class-validator';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config-ffmpeg.dto';
import { SystemConfigJobDto } from 'src/dtos/system-config-job.dto';
import { SystemConfigLibraryDto } from 'src/dtos/system-config-library.dto';
import { SystemConfigLoggingDto } from 'src/dtos/system-config-logging.dto';
import { SystemConfigMachineLearningDto } from 'src/dtos/system-config-machine-learning.dto';
import { SystemConfigMapDto } from 'src/dtos/system-config-map.dto';
import { SystemConfigNewVersionCheckDto } from 'src/dtos/system-config-new-version-check.dto';
import { SystemConfigOAuthDto } from 'src/dtos/system-config-oauth.dto';
import { SystemConfigPasswordLoginDto } from 'src/dtos/system-config-password-login.dto';
import { SystemConfigReverseGeocodingDto } from 'src/dtos/system-config-reverse-geocoding.dto';
import { SystemConfigServerDto } from 'src/dtos/system-config-server.dto';
import { SystemConfigStorageTemplateDto } from 'src/dtos/system-config-storage-template.dto';
import { SystemConfigThemeDto } from 'src/dtos/system-config-theme.dto';
import { SystemConfigThumbnailDto } from 'src/dtos/system-config-thumbnail.dto';
import { SystemConfigTrashDto } from 'src/dtos/system-config-trash.dto';
import { SystemConfigUserDto } from 'src/dtos/system-config-user.dto';
import { SystemConfig } from 'src/entities/system-config.entity';
export class SystemConfigDto implements SystemConfig {
@Type(() => SystemConfigFFmpegDto)
@ValidateNested()
@IsObject()
ffmpeg!: SystemConfigFFmpegDto;
@Type(() => SystemConfigLoggingDto)
@ValidateNested()
@IsObject()
logging!: SystemConfigLoggingDto;
@Type(() => SystemConfigMachineLearningDto)
@ValidateNested()
@IsObject()
machineLearning!: SystemConfigMachineLearningDto;
@Type(() => SystemConfigMapDto)
@ValidateNested()
@IsObject()
map!: SystemConfigMapDto;
@Type(() => SystemConfigNewVersionCheckDto)
@ValidateNested()
@IsObject()
newVersionCheck!: SystemConfigNewVersionCheckDto;
@Type(() => SystemConfigOAuthDto)
@ValidateNested()
@IsObject()
oauth!: SystemConfigOAuthDto;
@Type(() => SystemConfigPasswordLoginDto)
@ValidateNested()
@IsObject()
passwordLogin!: SystemConfigPasswordLoginDto;
@Type(() => SystemConfigReverseGeocodingDto)
@ValidateNested()
@IsObject()
reverseGeocoding!: SystemConfigReverseGeocodingDto;
@Type(() => SystemConfigStorageTemplateDto)
@ValidateNested()
@IsObject()
storageTemplate!: SystemConfigStorageTemplateDto;
@Type(() => SystemConfigJobDto)
@ValidateNested()
@IsObject()
job!: SystemConfigJobDto;
@Type(() => SystemConfigThumbnailDto)
@ValidateNested()
@IsObject()
thumbnail!: SystemConfigThumbnailDto;
@Type(() => SystemConfigTrashDto)
@ValidateNested()
@IsObject()
trash!: SystemConfigTrashDto;
@Type(() => SystemConfigThemeDto)
@ValidateNested()
@IsObject()
theme!: SystemConfigThemeDto;
@Type(() => SystemConfigLibraryDto)
@ValidateNested()
@IsObject()
library!: SystemConfigLibraryDto;
@Type(() => SystemConfigServerDto)
@ValidateNested()
@IsObject()
server!: SystemConfigServerDto;
@Type(() => SystemConfigUserDto)
@ValidateNested()
@IsObject()
user!: SystemConfigUserDto;
}
export function mapConfig(config: SystemConfig): SystemConfigDto {
return config;
}
+38
View File
@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity, TagType } from 'src/entities/tag.entity';
import { Optional } from 'src/validation';
export class CreateTagDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsEnum(TagType)
@IsNotEmpty()
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
type!: TagType;
}
export class UpdateTagDto {
@IsString()
@Optional()
name?: string;
}
export class TagResponseDto {
id!: string;
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
type!: string;
name!: string;
userId!: string;
}
export function mapTag(entity: TagEntity): TagResponseDto {
return {
id: entity.id,
type: entity.type,
name: entity.name,
userId: entity.userId,
};
}
+54
View File
@@ -0,0 +1,54 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { AssetOrder } from 'src/entities/album.entity';
import { TimeBucketSize } from 'src/interfaces/asset.repository';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class TimeBucketDto {
@IsNotEmpty()
@IsEnum(TimeBucketSize)
@ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' })
size!: TimeBucketSize;
@ValidateUUID({ optional: true })
userId?: string;
@ValidateUUID({ optional: true })
albumId?: string;
@ValidateUUID({ optional: true })
personId?: string;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isTrashed?: boolean;
@ValidateBoolean({ optional: true })
withStacked?: boolean;
@ValidateBoolean({ optional: true })
withPartners?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
order?: AssetOrder;
}
export class TimeBucketAssetDto extends TimeBucketDto {
@IsString()
timeBucket!: string;
}
export class TimeBucketResponseDto {
@ApiProperty({ type: 'string' })
timeBucket!: string;
@ApiProperty({ type: 'integer' })
count!: number;
}
+28
View File
@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { UploadFieldName } from 'src/dtos/asset.dto';
import { UserAvatarColor, UserEntity } from 'src/entities/user.entity';
export class CreateProfileImageDto {
@ApiProperty({ type: 'string', format: 'binary' })
[UploadFieldName.PROFILE_DATA]!: Express.Multer.File;
}
export class CreateProfileImageResponseDto {
userId!: string;
profileImagePath!: string;
}
export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
return {
userId: userId,
profileImagePath: profileImagePath,
};
}
export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return values[randomIndex] as UserAvatarColor;
};
+84
View File
@@ -0,0 +1,84 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
describe('update user DTO', () => {
it('should allow emails without a tld', async () => {
const someEmail = 'test@test';
const dto = plainToInstance(UpdateUserDto, {
email: someEmail,
id: '3fe388e4-2078-44d7-b36c-39d9dee3a657',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
});
describe('create user DTO', () => {
it('validates the email', async () => {
const params: Partial<CreateUserDto> = {
email: undefined,
password: 'password',
name: 'name',
};
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
let errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'invalid email';
dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'valid@email.com';
dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it('should allow emails without a tld', async () => {
const someEmail = 'test@test';
const dto = plainToInstance(CreateUserDto, {
email: someEmail,
password: 'some password',
name: 'some name',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
});
describe('create admin DTO', () => {
it('should allow emails without a tld', async () => {
const someEmail = 'test@test';
const dto = plainToInstance(CreateAdminDto, {
isAdmin: true,
email: someEmail,
password: 'some password',
name: 'some name',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
});
describe('create user oauth DTO', () => {
it('should allow emails without a tld', async () => {
const someEmail = 'test@test';
const dto = plainToInstance(CreateUserOAuthDto, {
email: someEmail,
oauthId: 'some oauth id',
name: 'some name',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
});
+169
View File
@@ -0,0 +1,169 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class CreateUserDto {
@IsEmail({ require_tld: false })
@Transform(toEmail)
email!: string;
@IsNotEmpty()
@IsString()
password!: string;
@IsNotEmpty()
@IsString()
name!: string;
@Optional({ nullable: true })
@IsString()
@Transform(toSanitized)
storageLabel?: string | null;
@ValidateBoolean({ optional: true })
memoriesEnabled?: boolean;
@Optional({ nullable: true })
@IsNumber()
@IsPositive()
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@ValidateBoolean({ optional: true })
shouldChangePassword?: boolean;
}
export class CreateAdminDto {
@IsNotEmpty()
isAdmin!: true;
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
email!: string;
@IsNotEmpty()
password!: string;
@IsNotEmpty()
name!: string;
}
export class CreateUserOAuthDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
email!: string;
@IsNotEmpty()
oauthId!: string;
name?: string;
}
export class DeleteUserDto {
@ValidateBoolean({ optional: true })
force?: boolean;
}
export class UpdateUserDto {
@Optional()
@IsEmail({ require_tld: false })
@Transform(toEmail)
email?: string;
@Optional()
@IsNotEmpty()
@IsString()
password?: string;
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@Optional()
@IsString()
@Transform(toSanitized)
storageLabel?: string;
@IsNotEmpty()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })
id!: string;
@ValidateBoolean({ optional: true })
isAdmin?: boolean;
@ValidateBoolean({ optional: true })
shouldChangePassword?: boolean;
@ValidateBoolean({ optional: true })
memoriesEnabled?: boolean;
@Optional()
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor;
@Optional({ nullable: true })
@IsNumber()
@IsPositive()
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
}
export class UserDto {
id!: string;
name!: string;
email!: string;
profileImagePath!: string;
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor!: UserAvatarColor;
}
export class UserResponseDto extends UserDto {
storageLabel!: string | null;
shouldChangePassword!: boolean;
isAdmin!: boolean;
createdAt!: Date;
deletedAt!: Date | null;
updatedAt!: Date;
oauthId!: string;
memoriesEnabled?: boolean;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
status!: string;
}
export const mapSimpleUser = (entity: UserEntity): UserDto => {
return {
id: entity.id,
email: entity.email,
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
};
};
export function mapUser(entity: UserEntity): UserResponseDto {
return {
...mapSimpleUser(entity),
storageLabel: entity.storageLabel,
shouldChangePassword: entity.shouldChangePassword,
isAdmin: entity.isAdmin,
createdAt: entity.createdAt,
deletedAt: entity.deletedAt,
updatedAt: entity.updatedAt,
oauthId: entity.oauthId,
memoriesEnabled: entity.memoriesEnabled,
quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
};
}