chore: upgrade to kysely 0.28.11 (#26744)

This commit is contained in:
Daniel Dietzler
2026-03-11 16:17:31 +01:00
committed by GitHub
parent 8764a1894b
commit 34ce68095d
50 changed files with 941 additions and 857 deletions
+9 -5
View File
@@ -1,18 +1,22 @@
import { mapAlbum } from 'src/dtos/album.dto';
import { AlbumFactory } from 'test/factories/album.factory';
import { getForAlbum } from 'test/mappers';
describe('mapAlbum', () => {
it('should set start and end dates', () => {
const startDate = new Date('2023-02-22T05:06:29.716Z');
const endDate = new Date('2025-01-01T01:02:03.456Z');
const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build();
const dto = mapAlbum(album, false);
expect(dto.startDate).toEqual(startDate);
expect(dto.endDate).toEqual(endDate);
const album = AlbumFactory.from()
.asset({ localDateTime: endDate }, (builder) => builder.exif())
.asset({ localDateTime: startDate }, (builder) => builder.exif())
.build();
const dto = mapAlbum(getForAlbum(album), false);
expect(dto.startDate).toEqual(startDate.toISOString());
expect(dto.endDate).toEqual(endDate.toISOString());
});
it('should not set start and end dates for empty assets', () => {
const dto = mapAlbum(AlbumFactory.create(), false);
const dto = mapAlbum(getForAlbum(AlbumFactory.create()), false);
expect(dto.startDate).toBeUndefined();
expect(dto.endDate).toBeUndefined();
});
+28 -21
View File
@@ -1,13 +1,16 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
import { ShallowDehydrateObject } from 'kysely';
import _ from 'lodash';
import { AlbumUser, AuthSharedLink, User } from 'src/database';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
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 { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { AlbumUserRole, AssetOrder } from 'src/enum';
import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date';
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
export class AlbumInfoDto {
@@ -151,10 +154,10 @@ export class AlbumResponseDto {
albumName!: string;
@ApiProperty({ description: 'Album description' })
description!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiProperty({ description: 'Creation date', format: 'date-time' })
createdAt!: string;
@ApiProperty({ description: 'Last update date', format: 'date-time' })
updatedAt!: string;
@ApiProperty({ description: 'Thumbnail asset ID' })
albumThumbnailAssetId!: string | null;
@ApiProperty({ description: 'Is shared album' })
@@ -172,12 +175,12 @@ export class AlbumResponseDto {
owner!: UserResponseDto;
@ApiProperty({ type: 'integer', description: 'Number of assets' })
assetCount!: number;
@ApiPropertyOptional({ description: 'Last modified asset timestamp' })
lastModifiedAssetTimestamp?: Date;
@ApiPropertyOptional({ description: 'Start date (earliest asset)' })
startDate?: Date;
@ApiPropertyOptional({ description: 'End date (latest asset)' })
endDate?: Date;
@ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' })
lastModifiedAssetTimestamp?: string;
@ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' })
startDate?: string;
@ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' })
endDate?: string;
@ApiProperty({ description: 'Activity feed enabled' })
isActivityEnabled!: boolean;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
@@ -191,8 +194,8 @@ export class AlbumResponseDto {
export type MapAlbumDto = {
albumUsers?: AlbumUser[];
assets?: MapAsset[];
sharedLinks?: AuthSharedLink[];
assets?: ShallowDehydrateObject<MapAsset>[];
sharedLinks?: ShallowDehydrateObject<AuthSharedLink>[];
albumName: string;
description: string;
albumThumbnailAssetId: string | null;
@@ -200,12 +203,16 @@ export type MapAlbumDto = {
updatedAt: Date;
id: string;
ownerId: string;
owner: User;
owner: ShallowDehydrateObject<User>;
isActivityEnabled: boolean;
order: AssetOrder;
};
export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
export const mapAlbum = (
entity: MaybeDehydrated<MapAlbumDto>,
withAssets: boolean,
auth?: AuthDto,
): AlbumResponseDto => {
const albumUsers: AlbumUserResponseDto[] = [];
if (entity.albumUsers) {
@@ -236,16 +243,16 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
albumName: entity.albumName,
description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
createdAt: asDateString(entity.createdAt),
updatedAt: asDateString(entity.updatedAt),
id: entity.id,
ownerId: entity.ownerId,
owner: mapUser(entity.owner),
albumUsers: albumUsersSorted,
shared: hasSharedUser || hasSharedLink,
hasSharedLink,
startDate,
endDate,
startDate: asDateString(startDate),
endDate: asDateString(endDate),
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled,
@@ -253,5 +260,5 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
};
};
export const mapAlbumWithAssets = (entity: MapAlbumDto) => mapAlbum(entity, true);
export const mapAlbumWithoutAssets = (entity: MapAlbumDto) => mapAlbum(entity, false);
export const mapAlbumWithAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, true);
export const mapAlbumWithoutAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, false);
+5 -4
View File
@@ -3,6 +3,7 @@ import { AssetEditAction } from 'src/dtos/editing.dto';
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
import { AssetFactory } from 'test/factories/asset.factory';
import { PersonFactory } from 'test/factories/person.factory';
import { getForAsset } from 'test/mappers';
describe('mapAsset', () => {
describe('peopleWithFaces', () => {
@@ -41,7 +42,7 @@ describe('mapAsset', () => {
})
.build();
const result = mapAsset(asset);
const result = mapAsset(getForAsset(asset));
expect(result.people).toBeDefined();
expect(result.people).toHaveLength(1);
@@ -80,7 +81,7 @@ describe('mapAsset', () => {
.edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } })
.build();
const result = mapAsset(asset);
const result = mapAsset(getForAsset(asset));
expect(result.unassignedFaces).toBeDefined();
expect(result.unassignedFaces).toHaveLength(1);
@@ -130,7 +131,7 @@ describe('mapAsset', () => {
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
.build();
const result = mapAsset(asset);
const result = mapAsset(getForAsset(asset));
expect(result.people).toBeDefined();
expect(result.people).toHaveLength(2);
@@ -179,7 +180,7 @@ describe('mapAsset', () => {
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
.build();
const result = mapAsset(asset);
const result = mapAsset(getForAsset(asset));
expect(result.people).toBeDefined();
expect(result.people).toHaveLength(1);
+28 -24
View File
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Selectable } from 'kysely';
import { Selectable, ShallowDehydrateObject } from 'kysely';
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -14,9 +14,10 @@ import {
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { ImageDimensions } from 'src/types';
import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { asDateString } from 'src/utils/date';
import { mimeTypes } from 'src/utils/mime-types';
import { ValidateEnum, ValidateUUID } from 'src/validation';
@@ -39,7 +40,7 @@ export class SanitizedAssetResponseDto {
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
example: '2024-01-15T14:30:00.000Z',
})
localDateTime!: Date;
localDateTime!: string;
@ApiProperty({ description: 'Video duration (for videos)' })
duration!: string;
@ApiPropertyOptional({ description: 'Live photo video ID' })
@@ -59,7 +60,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
description: 'The UTC timestamp when the asset was originally uploaded to Immich.',
example: '2024-01-15T20:30:00.000Z',
})
createdAt!: Date;
createdAt!: string;
@ApiProperty({ description: 'Device asset ID' })
deviceAssetId!: string;
@ApiProperty({ description: 'Device ID' })
@@ -86,7 +87,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.',
example: '2024-01-15T19:30:00.000Z',
})
fileCreatedAt!: Date;
fileCreatedAt!: string;
@ApiProperty({
type: 'string',
format: 'date-time',
@@ -94,7 +95,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
example: '2024-01-16T10:15:00.000Z',
})
fileModifiedAt!: Date;
fileModifiedAt!: string;
@ApiProperty({
type: 'string',
format: 'date-time',
@@ -102,7 +103,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
example: '2024-01-16T12:45:30.000Z',
})
updatedAt!: Date;
updatedAt!: string;
@ApiProperty({ description: 'Is favorite' })
isFavorite!: boolean;
@ApiProperty({ description: 'Is archived' })
@@ -151,13 +152,13 @@ export type MapAsset = {
deviceId: string;
duplicateId: string | null;
duration: string | null;
edits?: AssetEditActionItem[];
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
encodedVideoPath: string | null;
exifInfo?: Selectable<Exif> | null;
faces?: AssetFace[];
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
faces?: ShallowDehydrateObject<AssetFace>[];
fileCreatedAt: Date;
fileModifiedAt: Date;
files?: AssetFile[];
files?: ShallowDehydrateObject<AssetFile>[];
isExternal: boolean;
isFavorite: boolean;
isOffline: boolean;
@@ -167,11 +168,11 @@ export type MapAsset = {
localDateTime: Date;
originalFileName: string;
originalPath: string;
owner?: User | null;
owner?: ShallowDehydrateObject<User> | null;
ownerId: string;
stack?: Stack | null;
stack?: (ShallowDehydrateObject<Stack> & { assets: Stack['assets'] }) | null;
stackId: string | null;
tags?: Tag[];
tags?: ShallowDehydrateObject<Tag>[];
thumbhash: Buffer<ArrayBufferLike> | null;
type: AssetType;
width: number | null;
@@ -197,7 +198,7 @@ export type AssetMapOptions = {
};
const peopleWithFaces = (
faces?: AssetFace[],
faces?: MaybeDehydrated<AssetFace>[],
edits?: AssetEditActionItem[],
assetDimensions?: ImageDimensions,
): PersonWithFacesResponseDto[] => {
@@ -213,7 +214,10 @@ const peopleWithFaces = (
}
if (!peopleFaces.has(face.person.id)) {
peopleFaces.set(face.person.id, { ...mapPerson(face.person), faces: [] });
peopleFaces.set(face.person.id, {
...mapPerson(face.person),
faces: [],
});
}
const mappedFace = mapFacesWithoutPerson(face, edits, assetDimensions);
peopleFaces.get(face.person.id)!.faces.push(mappedFace);
@@ -234,7 +238,7 @@ const mapStack = (entity: { stack?: Stack | null }) => {
};
};
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options;
if (stripMetadata) {
@@ -243,7 +247,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
localDateTime: entity.localDateTime,
localDateTime: asDateString(entity.localDateTime),
duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false,
@@ -257,7 +261,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
return {
id: entity.id,
createdAt: entity.createdAt,
createdAt: asDateString(entity.createdAt),
deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined,
@@ -268,10 +272,10 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
localDateTime: entity.localDateTime,
updatedAt: entity.updatedAt,
fileCreatedAt: asDateString(entity.fileCreatedAt),
fileModifiedAt: asDateString(entity.fileModifiedAt),
localDateTime: asDateString(entity.localDateTime),
updatedAt: asDateString(entity.updatedAt),
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
isArchived: entity.visibility === AssetVisibility.Archive,
isTrashed: !!entity.deletedAt,
@@ -283,7 +287,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions),
unassignedFaces: entity.faces
?.filter((face) => !face.person)
.map((a) => mapFacesWithoutPerson(a, entity.edits, assetDimensions)),
.map((face) => mapFacesWithoutPerson(face, entity.edits, assetDimensions)),
checksum: hexOrBufferToBase64(entity.checksum)!,
stack: withStack ? mapStack(entity) : undefined,
isOffline: entity.isOffline,
+8 -6
View File
@@ -1,5 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Exif } from 'src/database';
import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date';
export class ExifResponseDto {
@ApiPropertyOptional({ description: 'Camera make' })
@@ -16,9 +18,9 @@ export class ExifResponseDto {
@ApiPropertyOptional({ description: 'Image orientation' })
orientation?: string | null = null;
@ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' })
dateTimeOriginal?: Date | null = null;
dateTimeOriginal?: string | null = null;
@ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' })
modifyDate?: Date | null = null;
modifyDate?: string | null = null;
@ApiPropertyOptional({ description: 'Time zone' })
timeZone?: string | null = null;
@ApiPropertyOptional({ description: 'Lens model' })
@@ -49,7 +51,7 @@ export class ExifResponseDto {
rating?: number | null = null;
}
export function mapExif(entity: Exif): ExifResponseDto {
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
return {
make: entity.make,
model: entity.model,
@@ -57,8 +59,8 @@ export function mapExif(entity: Exif): ExifResponseDto {
exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation,
dateTimeOriginal: entity.dateTimeOriginal,
modifyDate: entity.modifyDate,
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
modifyDate: asDateString(entity.modifyDate),
timeZone: entity.timeZone,
lensModel: entity.lensModel,
fNumber: entity.fNumber,
@@ -80,7 +82,7 @@ export function mapSanitizedExif(entity: Exif): ExifResponseDto {
return {
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation,
dateTimeOriginal: entity.dateTimeOriginal,
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
timeZone: entity.timeZone,
projectionType: entity.projectionType,
exifImageWidth: entity.exifImageWidth,
+13 -9
View File
@@ -9,8 +9,8 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { SourceType } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { ImageDimensions } from 'src/types';
import { asDateString } from 'src/utils/date';
import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { asBirthDateString, asDateString } from 'src/utils/date';
import { transformFaceBoundingBox } from 'src/utils/transform';
import {
IsDateStringFormat,
@@ -33,7 +33,7 @@ export class PersonCreateDto {
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
@IsDateStringFormat('yyyy-MM-dd')
@Optional({ nullable: true, emptyToNull: true })
birthDate?: Date | null;
birthDate?: string | null;
@ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' })
isHidden?: boolean;
@@ -105,8 +105,12 @@ export class PersonResponseDto {
thumbnailPath!: string;
@ApiProperty({ description: 'Is hidden' })
isHidden!: boolean;
@Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') })
updatedAt?: Date;
@Property({
description: 'Last update date',
format: 'date-time',
history: new HistoryBuilder().added('v1.107.0').stable('v2'),
})
updatedAt?: string;
@Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
isFavorite?: boolean;
@Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
@@ -222,21 +226,21 @@ export class PeopleResponseDto {
hasNextPage?: boolean;
}
export function mapPerson(person: Person): PersonResponseDto {
export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
return {
id: person.id,
name: person.name,
birthDate: asDateString(person.birthDate),
birthDate: asBirthDateString(person.birthDate),
thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden,
isFavorite: person.isFavorite,
color: person.color ?? undefined,
updatedAt: person.updatedAt,
updatedAt: asDateString(person.updatedAt),
};
}
export function mapFacesWithoutPerson(
face: Selectable<AssetFaceTable>,
face: MaybeDehydrated<Selectable<AssetFaceTable>>,
edits?: AssetEditActionItem[],
assetDimensions?: ImageDimensions,
): AssetFaceWithoutPersonResponseDto {
+9 -7
View File
@@ -1,6 +1,8 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
import { Tag } from 'src/database';
import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date';
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
export class TagCreateDto {
@@ -54,22 +56,22 @@ export class TagResponseDto {
name!: string;
@ApiProperty({ description: 'Tag value (full path)' })
value!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiProperty({ description: 'Creation date', format: 'date-time' })
createdAt!: string;
@ApiProperty({ description: 'Last update date', format: 'date-time' })
updatedAt!: string;
@ApiPropertyOptional({ description: 'Tag color (hex)' })
color?: string;
}
export function mapTag(entity: Tag): TagResponseDto {
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
return {
id: entity.id,
parentId: entity.parentId ?? undefined,
name: entity.value.split('/').at(-1) as string,
value: entity.value,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
createdAt: asDateString(entity.createdAt),
updatedAt: asDateString(entity.updatedAt),
color: entity.color ?? undefined,
};
}
+6 -5
View File
@@ -3,7 +3,8 @@ import { Transform } from 'class-transformer';
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
import { MaybeDehydrated, UserMetadataItem } from 'src/types';
import { asDateString } from 'src/utils/date';
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto {
@@ -47,8 +48,8 @@ export class UserResponseDto {
profileImagePath!: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' })
avatarColor!: UserAvatarColor;
@ApiProperty({ description: 'Profile change date' })
profileChangedAt!: Date;
@ApiProperty({ description: 'Profile change date', format: 'date-time' })
profileChangedAt!: string;
}
export class UserLicense {
@@ -68,14 +69,14 @@ const emailToAvatarColor = (email: string): UserAvatarColor => {
return values[randomIndex];
};
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponseDto => {
return {
id: entity.id,
email: entity.email,
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: entity.profileChangedAt,
profileChangedAt: asDateString(entity.profileChangedAt),
};
};