immich/server/src/dtos/album.dto.ts

196 lines
7.2 KiB
TypeScript

import { ShallowDehydrateObject } from 'kysely';
import { createZodDto } from 'nestjs-zod';
import { AlbumUser, AuthSharedLink } from 'src/database';
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
import { MaybeDehydrated } from 'src/types';
import { isoDatetimeToDate, stringToBool } from 'src/validation';
import z from 'zod';
const AlbumUserAddSchema = z
.object({
userId: z.uuidv4().describe('User ID'),
role: AlbumUserRoleSchema.default(AlbumUserRole.Editor).optional().describe('Album user role'),
})
.meta({ id: 'AlbumUserAddDto' });
const AddUsersSchema = z
.object({
albumUsers: z.array(AlbumUserAddSchema).min(1).describe('Album users to add'),
})
.meta({ id: 'AddUsersDto' });
const AlbumUserCreateSchema = z
.object({
userId: z.uuidv4().describe('User ID'),
role: AlbumUserRoleSchema,
})
.meta({ id: 'AlbumUserCreateDto' });
const CreateAlbumSchema = z
.object({
albumName: z.string().describe('Album name'),
description: z.string().optional().describe('Album description'),
albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'),
assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'),
})
.meta({ id: 'CreateAlbumDto' });
const AlbumsAddAssetsSchema = z
.object({
albumIds: z.array(z.uuidv4()).describe('Album IDs'),
assetIds: z.array(z.uuidv4()).describe('Asset IDs'),
})
.meta({ id: 'AlbumsAddAssetsDto' });
const AlbumsAddAssetsResponseSchema = z
.object({
success: z.boolean().describe('Operation success'),
error: BulkIdErrorReasonSchema.optional(),
})
.meta({ id: 'AlbumsAddAssetsResponseDto' });
const UpdateAlbumSchema = z
.object({
albumName: z.string().optional().describe('Album name'),
description: z.string().optional().describe('Album description'),
albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'),
isActivityEnabled: z.boolean().optional().describe('Enable activity feed'),
order: AssetOrderSchema.optional(),
})
.meta({ id: 'UpdateAlbumDto' });
const GetAlbumsSchema = z
.object({
shared: stringToBool
.optional()
.describe('Filter by shared status: true = only shared, false = not shared, undefined = all owned albums'),
assetId: z.uuidv4().optional().describe('Filter albums containing this asset ID (ignores shared parameter)'),
})
.meta({ id: 'GetAlbumsDto' });
const AlbumStatisticsResponseSchema = z
.object({
owned: z.int().min(0).describe('Number of owned albums'),
shared: z.int().min(0).describe('Number of shared albums'),
notShared: z.int().min(0).describe('Number of non-shared albums'),
})
.meta({ id: 'AlbumStatisticsResponseDto' });
const UpdateAlbumUserSchema = z
.object({
role: AlbumUserRoleSchema,
})
.meta({ id: 'UpdateAlbumUserDto' });
const AlbumUserResponseSchema = z
.object({
user: UserResponseSchema,
role: AlbumUserRoleSchema,
})
.meta({ id: 'AlbumUserResponseDto' });
const ContributorCountResponseSchema = z
.object({
userId: z.string().describe('User ID'),
assetCount: z.int().min(0).describe('Number of assets contributed'),
})
.meta({ id: 'ContributorCountResponseDto' });
export const AlbumResponseSchema = z
.object({
id: z.string().describe('Album ID'),
albumName: z.string().describe('Album name'),
description: z.string().describe('Album description'),
createdAt: isoDatetimeToDate.describe('Creation date'),
updatedAt: isoDatetimeToDate.describe('Last update date'),
albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
shared: z.boolean().describe('Is shared album'),
albumUsers: z
.array(AlbumUserResponseSchema)
.min(1)
.describe(
'First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.',
),
hasSharedLink: z.boolean().describe('Has shared link'),
assetCount: z.int().min(0).describe('Number of assets'),
lastModifiedAssetTimestamp: isoDatetimeToDate.optional().describe('Last modified asset timestamp'),
startDate: isoDatetimeToDate.optional().describe('Start date (earliest asset)'),
endDate: isoDatetimeToDate.optional().describe('End date (latest asset)'),
isActivityEnabled: z.boolean().describe('Activity feed enabled'),
order: AssetOrderSchema.optional(),
contributorCounts: z.array(ContributorCountResponseSchema).optional(),
})
.meta({ id: 'AlbumResponseDto' });
export class AddUsersDto extends createZodDto(AddUsersSchema) {}
export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {}
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
export class AlbumsAddAssetsDto extends createZodDto(AlbumsAddAssetsSchema) {}
export class AlbumsAddAssetsResponseDto extends createZodDto(AlbumsAddAssetsResponseSchema) {}
export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema, { codec: true }) {}
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
export type MapAlbumDto = {
albumUsers?: AlbumUser[];
assets?: ShallowDehydrateObject<MapAsset>[];
sharedLinks?: ShallowDehydrateObject<AuthSharedLink>[];
albumName: string;
description: string;
albumThumbnailAssetId: string | null;
createdAt: Date;
updatedAt: Date;
id: string;
isActivityEnabled: boolean;
order: AssetOrder;
};
export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto => {
const albumUsers: AlbumUserResponseDto[] = [];
if (entity.albumUsers) {
for (const albumUser of entity.albumUsers) {
const user = mapUser(albumUser.user);
albumUsers.push({
user,
role: albumUser.role,
});
}
}
const assets = entity.assets || [];
const hasSharedLink = !!entity.sharedLinks && entity.sharedLinks.length > 0;
const hasSharedUser = albumUsers.length > 1;
let startDate = assets.at(0)?.localDateTime;
let endDate = assets.at(-1)?.localDateTime;
// 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: new Date(entity.createdAt),
updatedAt: new Date(entity.updatedAt),
id: entity.id,
albumUsers,
shared: hasSharedUser || hasSharedLink,
hasSharedLink,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled,
order: entity.order,
};
};