pull/28810/merge
Timon 2026-06-03 22:38:43 +00:00 committed by GitHub
commit 0b6b4b08dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 75 additions and 42 deletions

View File

@ -6,7 +6,7 @@ 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 { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import { stringToBool } from 'src/validation';
import z from 'zod';
@ -195,14 +195,14 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
albumName: entity.albumName,
description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: asDateString(entity.createdAt),
updatedAt: asDateString(entity.updatedAt),
createdAt: asDateTimeString(entity.createdAt),
updatedAt: asDateTimeString(entity.updatedAt),
id: entity.id,
albumUsers,
shared: hasSharedUser || hasSharedLink,
hasSharedLink,
startDate: asDateString(startDate),
endDate: asDateString(endDate),
startDate: asDateTimeString(startDate),
endDate: asDateTimeString(endDate),
assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled,
order: entity.order,

View File

@ -18,7 +18,7 @@ import {
} from 'src/enum';
import { MaybeDehydrated } from 'src/types';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import { mimeTypes } from 'src/utils/mime-types';
import z from 'zod';
@ -199,7 +199,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
localDateTime: asDateString(entity.localDateTime),
localDateTime: asDateTimeString(entity.localDateTime),
duration: entity.duration,
livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false,
@ -211,7 +211,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
return {
id: entity.id,
createdAt: asDateString(entity.createdAt),
createdAt: asDateTimeString(entity.createdAt),
ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined,
libraryId: entity.libraryId,
@ -220,10 +220,10 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
fileCreatedAt: asDateString(entity.fileCreatedAt),
fileModifiedAt: asDateString(entity.fileModifiedAt),
localDateTime: asDateString(entity.localDateTime),
updatedAt: asDateString(entity.updatedAt),
fileCreatedAt: asDateTimeString(entity.fileCreatedAt),
fileModifiedAt: asDateTimeString(entity.fileModifiedAt),
localDateTime: asDateTimeString(entity.localDateTime),
updatedAt: asDateTimeString(entity.updatedAt),
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
isArchived: entity.visibility === AssetVisibility.Archive,
isTrashed: !!entity.deletedAt,

View File

@ -1,7 +1,7 @@
import { createZodDto } from 'nestjs-zod';
import { Exif } from 'src/database';
import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import z from 'zod';
export const ExifResponseSchema = z
@ -44,8 +44,8 @@ export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation,
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
modifyDate: asDateString(entity.modifyDate),
dateTimeOriginal: asDateTimeString(entity.dateTimeOriginal),
modifyDate: asDateTimeString(entity.modifyDate),
timeZone: entity.timeZone,
lensModel: entity.lensModel,
fNumber: entity.fNumber,

View File

@ -7,7 +7,7 @@ import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { SourceTypeSchema } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { asBirthDateString, asDateString } from 'src/utils/date';
import { asDateString, asDateTimeString } from 'src/utils/date';
import { transformFaceBoundingBox } from 'src/utils/transform';
import { hexColor, stringToBool } from 'src/validation';
import z from 'zod';
@ -175,12 +175,12 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
return {
id: person.id,
name: person.name,
birthDate: asBirthDateString(person.birthDate),
birthDate: asDateString(person.birthDate),
thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden,
isFavorite: person.isFavorite,
color: person.color ?? undefined,
updatedAt: asDateString(person.updatedAt),
updatedAt: asDateTimeString(person.updatedAt),
};
}

View File

@ -1,7 +1,7 @@
import { createZodDto } from 'nestjs-zod';
import { Tag } from 'src/database';
import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import { hexColor } from 'src/validation';
import z from 'zod';
@ -65,8 +65,8 @@ export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
parentId: entity.parentId ?? undefined,
name: entity.value.split('/').at(-1) as string,
value: entity.value,
createdAt: asDateString(entity.createdAt),
updatedAt: asDateString(entity.updatedAt),
createdAt: asDateTimeString(entity.createdAt),
updatedAt: asDateTimeString(entity.updatedAt),
color: entity.color ?? undefined,
};
}

View File

@ -3,7 +3,7 @@ import { User, UserAdmin } from 'src/database';
import { pinCodeRegex } from 'src/dtos/auth.dto';
import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum';
import { MaybeDehydrated, UserMetadataItem } from 'src/types';
import { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import { isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
import z from 'zod';
@ -61,7 +61,7 @@ export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponse
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: asDateString(entity.profileChangedAt),
profileChangedAt: asDateTimeString(entity.profileChangedAt),
};
};

View File

@ -18,7 +18,7 @@ import { AlbumUserRole, Permission } from 'src/enum';
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util';
import { asDateString } from 'src/utils/date';
import { asDateTimeString } from 'src/utils/date';
import { getPreferences } from 'src/utils/preferences';
@Injectable()
@ -59,11 +59,11 @@ export class AlbumService extends BaseService {
return albums.map((album) => ({
...mapAlbum(album),
sharedLinks: undefined,
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
startDate: asDateTimeString(albumMetadata[album.id]?.startDate ?? undefined),
endDate: asDateTimeString(albumMetadata[album.id]?.endDate ?? undefined),
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
lastModifiedAssetTimestamp: asDateString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined),
lastModifiedAssetTimestamp: asDateTimeString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined),
}));
}
@ -79,10 +79,10 @@ export class AlbumService extends BaseService {
return {
...mapAlbum(album),
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
startDate: asDateTimeString(albumMetadataForIds?.startDate ?? undefined),
endDate: asDateTimeString(albumMetadataForIds?.endDate ?? undefined),
assetCount: albumMetadataForIds?.assetCount ?? 0,
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
lastModifiedAssetTimestamp: asDateTimeString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
};
}

View File

@ -0,0 +1,33 @@
import { asDateString, asDateTimeString } from 'src/utils/date';
import { describe, expect, it } from 'vitest';
describe('asDateString', () => {
it('should return null for null input', () => {
expect(asDateString(null)).toBeNull();
});
it('should pass through a pre-serialized string unchanged', () => {
expect(asDateString('2000-01-15')).toBe('2000-01-15');
});
it('should return the local calendar date, not the UTC date', () => {
const date = new Date(2000, 0, 15); // 15 Jan 2000, local midnight
expect(asDateString(date)).toBe('2000-01-15');
});
});
describe('asDateTimeString', () => {
it('should return null for null input', () => {
expect(asDateTimeString(null)).toBeNull();
});
it('should pass through a pre-serialized string unchanged', () => {
const iso = '2000-01-15T12:00:00.000Z';
expect(asDateTimeString(iso)).toBe(iso);
});
it('should return an ISO 8601 datetime string for a Date', () => {
const date = new Date('2000-01-15T12:00:00.000Z');
expect(asDateTimeString(date)).toBe('2000-01-15T12:00:00.000Z');
});
});

View File

@ -1,23 +1,18 @@
import { DateTime } from 'luxon';
import { isoDateToDate, isoDatetimeToDate } from 'src/validation';
/**
* Convert a date to a ISO 8601 datetime string.
* @param x - The date to convert.
* @returns The ISO 8601 datetime string.
* @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDatetimeToDate` in validation.ts will handle the conversion instead.
*/
export const asDateString = <T extends Date | string | undefined | null>(x: T) => {
return x instanceof Date ? x.toISOString() : (x as Exclude<T, Date>);
export const asDateTimeString = <T extends Date | string | undefined | null>(x: T) => {
return x instanceof Date ? isoDatetimeToDate.encode(x) : (x as Exclude<T, Date>);
};
/**
* Convert a date to a date string.
* @param x - The date to convert.
* @returns The date string.
* @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDateToDate` in validation.ts will handle the conversion instead.
* Convert a date to a date string (yyyy-mm-dd).
*/
export const asBirthDateString = (x: Date | string | null): string | null => {
return x instanceof Date ? x.toISOString().split('T')[0] : x;
export const asDateString = (x: Date | string | null): string | null => {
return x instanceof Date ? isoDateToDate.encode(x) : x;
};
export const extractTimeZone = (dateTimeOriginal?: string | null) => {

View File

@ -166,7 +166,12 @@ export const isoDateToDate = z
z.date(),
{
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString().slice(0, 10),
encode: (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
},
},
)
.meta({ example: '2024-01-01' });