1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

fix(server): keep full datetime precision from database

This commit is contained in:
Fynn Petersen-Frey 2024-06-25 14:45:43 +02:00
parent d7a33c8ec2
commit 94b9013da5
44 changed files with 120 additions and 99 deletions

View file

@ -18,7 +18,8 @@ export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
export class ActivityResponseDto { export class ActivityResponseDto {
id!: string; id!: string;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
createdAt!: string;
type!: ReactionType; type!: ReactionType;
user!: UserResponseDto; user!: UserResponseDto;
assetId!: string | null; assetId!: string | null;

View file

@ -123,8 +123,10 @@ export class AlbumResponseDto {
ownerId!: string; ownerId!: string;
albumName!: string; albumName!: string;
description!: string; description!: string;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: Date; createdAt!: string;
@ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
albumThumbnailAssetId!: string | null; albumThumbnailAssetId!: string | null;
shared!: boolean; shared!: boolean;
albumUsers!: AlbumUserResponseDto[]; albumUsers!: AlbumUserResponseDto[];

View file

@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
import { Optional } from 'src/validation'; import { Optional } from 'src/validation';
export class APIKeyCreateDto { export class APIKeyCreateDto {
@ -21,6 +22,8 @@ export class APIKeyCreateResponseDto {
export class APIKeyResponseDto { export class APIKeyResponseDto {
id!: string; id!: string;
name!: string; name!: string;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: Date; createdAt!: string;
@ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
} }

View file

@ -39,7 +39,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
originalFileName!: string; originalFileName!: string;
fileCreatedAt!: Date; fileCreatedAt!: Date;
fileModifiedAt!: Date; fileModifiedAt!: Date;
updatedAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
isFavorite!: boolean; isFavorite!: boolean;
isArchived!: boolean; isArchived!: boolean;
isTrashed!: boolean; isTrashed!: boolean;

View file

@ -3,13 +3,13 @@ import { Type } from 'class-transformer';
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
import { EntityType } from 'src/entities/audit.entity'; import { EntityType } from 'src/entities/audit.entity';
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; import { Optional, ValidateUUID } from 'src/validation';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
export class AuditDeletesDto { export class AuditDeletesDto {
@ValidateDate() @ApiProperty({ type: 'string', format: 'date-time' })
after!: Date; after!: string;
@ApiProperty({ enum: EntityType, enumName: 'EntityType' }) @ApiProperty({ enum: EntityType, enumName: 'EntityType' })
@IsEnum(EntityType) @IsEnum(EntityType)

View file

@ -105,8 +105,10 @@ export class LibraryResponseDto {
exclusionPatterns!: string[]; exclusionPatterns!: string[];
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: Date; createdAt!: string;
@ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
refreshedAt!: Date | null; refreshedAt!: Date | null;
} }

View file

@ -55,9 +55,12 @@ export class MemoryCreateDto extends MemoryBaseDto {
export class MemoryResponseDto { export class MemoryResponseDto {
id!: string; id!: string;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: Date; createdAt!: string;
deletedAt?: Date; @ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
@ApiProperty({ type: 'string', format: 'date-time' })
deletedAt?: string;
memoryAt!: Date; memoryAt!: Date;
seenAt?: Date; seenAt?: Date;
ownerId!: string; ownerId!: string;

View file

@ -11,8 +11,8 @@ export class SessionResponseDto {
export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({ export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({
id: entity.id, id: entity.id,
createdAt: entity.createdAt.toISOString(), createdAt: entity.createdAt,
updatedAt: entity.updatedAt.toISOString(), updatedAt: entity.updatedAt,
current: currentId === entity.id, current: currentId === entity.id,
deviceOS: entity.deviceOS, deviceOS: entity.deviceOS,
deviceType: entity.deviceType, deviceType: entity.deviceType,

View file

@ -86,7 +86,8 @@ export class SharedLinkResponseDto {
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType }) @ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
type!: SharedLinkType; type!: SharedLinkType;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
createdAt!: string;
expiresAt!: Date | null; expiresAt!: Date | null;
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
album?: AlbumResponseDto; album?: AlbumResponseDto;

View file

@ -1,14 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsPositive } from 'class-validator'; import { IsInt, IsPositive } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { ValidateDate, ValidateUUID } from 'src/validation'; import { ValidateUUID } from 'src/validation';
export class AssetFullSyncDto { export class AssetFullSyncDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
lastId?: string; lastId?: string;
@ValidateDate() @ApiProperty({ type: 'string', format: 'date-time' })
updatedUntil!: Date; updatedUntil!: string;
@IsInt() @IsInt()
@IsPositive() @IsPositive()
@ -20,8 +20,8 @@ export class AssetFullSyncDto {
} }
export class AssetDeltaSyncDto { export class AssetDeltaSyncDto {
@ValidateDate() @ApiProperty({ type: 'string', format: 'date-time' })
updatedAfter!: Date; updatedAfter!: string;
@ValidateUUID({ each: true }) @ValidateUUID({ each: true })
userIds!: string[]; userIds!: string[];

View file

@ -120,9 +120,12 @@ export class UserAdminResponseDto extends UserResponseDto {
storageLabel!: string | null; storageLabel!: string | null;
shouldChangePassword!: boolean; shouldChangePassword!: boolean;
isAdmin!: boolean; isAdmin!: boolean;
createdAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
deletedAt!: Date | null; createdAt!: string;
updatedAt!: Date; @ApiProperty({ type: 'string', format: 'date-time' })
deletedAt!: string | null;
@ApiProperty({ type: 'string', format: 'date-time' })
updatedAt!: string;
oauthId!: string; oauthId!: string;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes!: number | null; quotaSizeInBytes!: number | null;

View file

@ -20,10 +20,10 @@ export class ActivityEntity {
id!: string; id!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@Column() @Column()
albumId!: string; albumId!: string;

View file

@ -39,13 +39,13 @@ export class AlbumEntity {
description!: string; description!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@DeleteDateColumn({ type: 'timestamptz' }) @DeleteDateColumn({ type: 'timestamptz' })
deletedAt!: Date | null; deletedAt!: string | null;
@ManyToOne(() => AssetEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) @ManyToOne(() => AssetEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
albumThumbnailAsset!: AssetEntity | null; albumThumbnailAsset!: AssetEntity | null;

View file

@ -19,8 +19,8 @@ export class APIKeyEntity {
userId!: string; userId!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
} }

View file

@ -84,13 +84,13 @@ export class AssetEntity {
encodedVideoPath!: string | null; encodedVideoPath!: string | null;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@DeleteDateColumn({ type: 'timestamptz', nullable: true }) @DeleteDateColumn({ type: 'timestamptz', nullable: true })
deletedAt!: Date | null; deletedAt!: string | null;
@Index('idx_asset_file_created_at') @Index('idx_asset_file_created_at')
@Column({ type: 'timestamptz' }) @Column({ type: 'timestamptz' })

View file

@ -30,5 +30,5 @@ export class AuditEntity {
ownerId!: string; ownerId!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
} }

View file

@ -37,13 +37,13 @@ export class LibraryEntity {
exclusionPatterns!: string[]; exclusionPatterns!: string[];
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@DeleteDateColumn({ type: 'timestamptz' }) @DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date; deletedAt?: string;
@Column({ type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
refreshedAt!: Date | null; refreshedAt!: Date | null;

View file

@ -29,13 +29,13 @@ export class MemoryEntity<T extends MemoryType = MemoryType> {
id!: string; id!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@DeleteDateColumn({ type: 'timestamptz' }) @DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date; deletedAt?: string;
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
owner!: UserEntity; owner!: UserEntity;

View file

@ -18,10 +18,10 @@ export class PartnerEntity {
sharedWith!: UserEntity; sharedWith!: UserEntity;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
inTimeline!: boolean; inTimeline!: boolean;

View file

@ -18,10 +18,10 @@ export class PersonEntity {
id!: string; id!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@Column() @Column()
ownerId!: string; ownerId!: string;

View file

@ -16,10 +16,10 @@ export class SessionEntity {
user!: UserEntity; user!: UserEntity;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@Column({ default: '' }) @Column({ default: '' })
deviceType!: string; deviceType!: string;

View file

@ -38,7 +38,7 @@ export class SharedLinkEntity {
type!: SharedLinkType; type!: SharedLinkType;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@Column({ type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
expiresAt!: Date | null; expiresAt!: Date | null;

View file

@ -47,16 +47,16 @@ export class UserEntity {
shouldChangePassword!: boolean; shouldChangePassword!: boolean;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: string;
@DeleteDateColumn({ type: 'timestamptz' }) @DeleteDateColumn({ type: 'timestamptz' })
deletedAt!: Date | null; deletedAt!: string | null;
@Column({ type: 'varchar', default: UserStatus.ACTIVE }) @Column({ type: 'varchar', default: UserStatus.ACTIVE })
status!: UserStatus; status!: UserStatus;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: string;
@OneToMany(() => TagEntity, (tag) => tag.user) @OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[]; tags!: TagEntity[];

View file

@ -123,13 +123,13 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
export interface AssetFullSyncOptions { export interface AssetFullSyncOptions {
ownerId: string; ownerId: string;
lastId?: string; lastId?: string;
updatedUntil: Date; updatedUntil: string;
limit: number; limit: number;
} }
export interface AssetDeltaSyncOptions { export interface AssetDeltaSyncOptions {
userIds: string[]; userIds: string[];
updatedAfter: Date; updatedAfter: string;
limit: number; limit: number;
} }

View file

@ -9,6 +9,6 @@ export interface AuditSearch {
} }
export interface IAuditRepository { export interface IAuditRepository {
getAfter(since: Date, options: AuditSearch): Promise<string[]>; getAfter(since: string, options: AuditSearch): Promise<string[]>;
removeBefore(before: Date): Promise<void>; removeBefore(before: Date): Promise<void>;
} }

View file

@ -10,7 +10,7 @@ import { In, LessThan, MoreThan, Repository } from 'typeorm';
export class AuditRepository implements IAuditRepository { export class AuditRepository implements IAuditRepository {
constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {} constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {}
async getAfter(since: Date, options: AuditSearch): Promise<string[]> { async getAfter(since: string, options: AuditSearch): Promise<string[]> {
const records = await this.repository const records = await this.repository
.createQueryBuilder('audit') .createQueryBuilder('audit')
.where({ .where({
@ -28,6 +28,6 @@ export class AuditRepository implements IAuditRepository {
} }
async removeBefore(before: Date): Promise<void> { async removeBefore(before: Date): Promise<void> {
await this.repository.delete({ createdAt: LessThan(before) }); await this.repository.delete({ createdAt: LessThan(before.toISOString()) });
} }
} }

View file

@ -13,7 +13,7 @@ export class SessionRepository implements ISessionRepository {
@GenerateSql({ params: [DummyValue.DATE] }) @GenerateSql({ params: [DummyValue.DATE] })
search(options: SessionSearchOptions): Promise<SessionEntity[]> { search(options: SessionSearchOptions): Promise<SessionEntity[]> {
return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } }); return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore.toISOString()) } });
} }
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.STRING] })

View file

@ -569,7 +569,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
albumThumbnailAssetId: 'asset-1', albumThumbnailAssetId: 'asset-1',
}); });
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
@ -595,7 +595,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
albumThumbnailAssetId: 'asset-id', albumThumbnailAssetId: 'asset-id',
}); });
expect(albumMock.addAssetIds).toHaveBeenCalled(); expect(albumMock.addAssetIds).toHaveBeenCalled();
@ -617,7 +617,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
albumThumbnailAssetId: 'asset-1', albumThumbnailAssetId: 'asset-1',
}); });
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
@ -658,7 +658,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
albumThumbnailAssetId: 'asset-1', albumThumbnailAssetId: 'asset-1',
}); });
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
@ -681,7 +681,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
albumThumbnailAssetId: 'asset-1', albumThumbnailAssetId: 'asset-1',
}); });
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
@ -746,7 +746,7 @@ describe(AlbumService.name, () => {
{ success: true, id: 'asset-id' }, { success: true, id: 'asset-id' },
]); ]);
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) }); expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(String) });
expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']); expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
}); });
@ -790,7 +790,7 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123', id: 'album-123',
updatedAt: expect.any(Date), updatedAt: expect.any(String),
}); });
expect(albumMock.updateThumbnails).toHaveBeenCalled(); expect(albumMock.updateThumbnails).toHaveBeenCalled();
}); });

View file

@ -185,7 +185,7 @@ export class AlbumService {
if (firstNewAssetId) { if (firstNewAssetId) {
await this.albumRepository.update({ await this.albumRepository.update({
id, id,
updatedAt: new Date(), updatedAt: new Date().toISOString(),
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
}); });
} }
@ -211,7 +211,7 @@ export class AlbumService {
const removedIds = results.filter(({ success }) => success).map(({ id }) => id); const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
if (removedIds.length > 0) { if (removedIds.length > 0) {
await this.albumRepository.update({ id, updatedAt: new Date() }); await this.albumRepository.update({ id, updatedAt: new Date().toISOString() });
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) { if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
await this.albumRepository.updateThumbnails(); await this.albumRepository.updateThumbnails();
} }

View file

@ -265,7 +265,7 @@ describe(AssetService.name, () => {
ids: [], ids: [],
stackParentId: 'parent', stackParentId: 'parent',
}), }),
expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { updatedAt: expect.any(Date) }); expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { updatedAt: expect.any(String) });
}); });
it('should update parent asset when children are removed', async () => { it('should update parent asset when children are removed', async () => {
@ -285,7 +285,7 @@ describe(AssetService.name, () => {
}); });
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['child-1']), { stack: null }); expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['child-1']), { stack: null });
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), {
updatedAt: expect.any(Date), updatedAt: expect.any(String),
}); });
expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1'); expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1');
}); });
@ -316,7 +316,7 @@ describe(AssetService.name, () => {
]), ]),
primaryAsset: undefined, primaryAsset: undefined,
}); });
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2', 'parent'], { updatedAt: expect.any(Date) }); expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2', 'parent'], { updatedAt: expect.any(String) });
}); });
it('remove stack for removed children', async () => { it('remove stack for removed children', async () => {
@ -353,7 +353,7 @@ describe(AssetService.name, () => {
primaryAssetId: 'parent', primaryAssetId: 'parent',
}); });
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], { expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], {
updatedAt: expect.any(Date), updatedAt: expect.any(String),
}); });
}); });
@ -532,7 +532,7 @@ describe(AssetService.name, () => {
expect(assetStackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' }); expect(assetStackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' });
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], { expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], {
updatedAt: expect.any(Date), updatedAt: expect.any(String),
}); });
}); });
}); });

View file

@ -191,7 +191,7 @@ export class AssetService {
// All the unique parent's -> parent is set to null // All the unique parent's -> parent is set to null
await this.assetRepository.updateAll( await this.assetRepository.updateAll(
assets.filter((a) => !!a.stack?.primaryAssetId).map((a) => a.stack!.primaryAssetId!), assets.filter((a) => !!a.stack?.primaryAssetId).map((a) => a.stack!.primaryAssetId!),
{ updatedAt: new Date() }, { updatedAt: new Date().toISOString() },
); );
} else if (options.stackParentId) { } else if (options.stackParentId) {
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack //Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack
@ -225,7 +225,7 @@ export class AssetService {
// Merge stacks // Merge stacks
options.stackParentId = undefined; options.stackParentId = undefined;
(options as Partial<AssetEntity>).updatedAt = new Date(); (options as Partial<AssetEntity>).updatedAt = new Date().toISOString();
} }
for (const id of ids) { for (const id of ids) {
@ -372,7 +372,9 @@ export class AssetService {
newParentId, newParentId,
oldParentId, oldParentId,
]); ]);
await this.assetRepository.updateAll([oldParentId, newParentId, ...childIds], { updatedAt: new Date() }); await this.assetRepository.updateAll([oldParentId, newParentId, ...childIds], {
updatedAt: new Date().toISOString(),
});
} }
async run(auth: AuthDto, dto: AssetJobsDto) { async run(auth: AuthDto, dto: AssetJobsDto) {

View file

@ -58,7 +58,7 @@ describe(AuditService.name, () => {
it('should require full sync if the request is older than 100 days', async () => { it('should require full sync if the request is older than 100 days', async () => {
auditMock.getAfter.mockResolvedValue([]); auditMock.getAfter.mockResolvedValue([]);
const date = new Date(2022, 0, 1); const date = new Date(2022, 0, 1).toISOString();
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({ await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
needsFullSync: true, needsFullSync: true,
ids: [], ids: [],

View file

@ -59,7 +59,7 @@ export class AuditService {
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,
}); });
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.after)); const duration = DateTime.now().diff(DateTime.fromISO(dto.after));
return { return {
needsFullSync: duration > AUDIT_LOG_MAX_DURATION, needsFullSync: duration > AUDIT_LOG_MAX_DURATION,

View file

@ -331,7 +331,7 @@ describe('AuthService', () => {
sessionMock.update.mockResolvedValue(sessionStub.valid); sessionMock.update.mockResolvedValue(sessionStub.valid);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toBeDefined(); await expect(sut.validate(headers, {})).resolves.toBeDefined();
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(String) });
}); });
}); });

View file

@ -382,10 +382,10 @@ export class AuthService {
if (session?.user) { if (session?.user) {
const now = DateTime.now(); const now = DateTime.now();
const updatedAt = DateTime.fromJSDate(session.updatedAt); const updatedAt = DateTime.fromISO(session.updatedAt);
const diff = now.diff(updatedAt, ['hours']); const diff = now.diff(updatedAt, ['hours']);
if (diff.hours > 1) { if (diff.hours > 1) {
await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); await this.sessionRepository.update({ id: session.id, updatedAt: new Date().toISOString() });
} }
return { user: session.user, session: session }; return { user: session.user, session: session };

View file

@ -74,7 +74,7 @@ export class MemoryService {
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.repository.update({ id, updatedAt: new Date() }); await this.repository.update({ id, updatedAt: new Date().toISOString() });
} }
return results; return results;
@ -89,7 +89,7 @@ export class MemoryService {
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.repository.update({ id, updatedAt: new Date() }); await this.repository.update({ id, updatedAt: new Date().toISOString() });
} }
return results; return results;

View file

@ -38,8 +38,8 @@ describe('SessionService', () => {
it('should delete sessions', async () => { it('should delete sessions', async () => {
sessionMock.search.mockResolvedValue([ sessionMock.search.mockResolvedValue([
{ {
createdAt: new Date('1970-01-01T00:00:00.00Z'), createdAt: new Date('1970-01-01T00:00:00.00Z').toISOString(),
updatedAt: new Date('1970-01-02T00:00:00.00Z'), updatedAt: new Date('1970-01-02T00:00:00.00Z').toISOString(),
deviceOS: '', deviceOS: '',
deviceType: '', deviceType: '',
id: '123', id: '123',

View file

@ -14,7 +14,7 @@ import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const untilDate = new Date(2024); const untilDate = new Date(2024).toISOString();
const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true }; const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true };
describe(SyncService.name, () => { describe(SyncService.name, () => {
@ -55,7 +55,7 @@ describe(SyncService.name, () => {
it('should return a response requiring a full sync when partners are out of sync', async () => { it('should return a response requiring a full sync when partners are out of sync', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
await expect( await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, { updatedAfter: new Date().toISOString(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(auditMock.getAfter).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
@ -64,7 +64,10 @@ describe(SyncService.name, () => {
it('should return a response requiring a full sync when last sync was too long ago', async () => { it('should return a response requiring a full sync when last sync was too long ago', async () => {
partnerMock.getAll.mockResolvedValue([]); partnerMock.getAll.mockResolvedValue([]);
await expect( await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, {
updatedAfter: new Date(2000).toISOString(),
userIds: [authStub.user1.user.id],
}),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(auditMock.getAfter).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0);

View file

@ -42,7 +42,7 @@ export class SyncService {
async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
// app has not synced in the last 100 days // app has not synced in the last 100 days
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter)); const duration = DateTime.now().diff(DateTime.fromISO(dto.updatedAfter));
if (duration > AUDIT_LOG_MAX_DURATION) { if (duration > AUDIT_LOG_MAX_DURATION) {
return FULL_SYNC; return FULL_SYNC;
} }

View file

@ -97,7 +97,7 @@ describe(UserAdminService.name, () => {
await sut.update(authStub.admin, userStub.user1.id, { storageLabel: '' }); await sut.update(authStub.admin, userStub.user1.id, { storageLabel: '' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
storageLabel: null, storageLabel: null,
updatedAt: expect.any(Date), updatedAt: expect.any(String),
}); });
}); });
@ -157,7 +157,7 @@ describe(UserAdminService.name, () => {
await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUserAdmin(userStub.user1)); await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.DELETED, status: UserStatus.DELETED,
deletedAt: expect.any(Date), deletedAt: expect.any(String),
}); });
}); });
@ -171,7 +171,7 @@ describe(UserAdminService.name, () => {
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.REMOVING, status: UserStatus.REMOVING,
deletedAt: expect.any(Date), deletedAt: expect.any(String),
}); });
expect(jobMock.queue).toHaveBeenCalledWith({ expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.USER_DELETION, name: JobName.USER_DELETION,

View file

@ -85,7 +85,7 @@ export class UserAdminService {
dto.storageLabel = null; dto.storageLabel = null;
} }
const updatedUser = await this.userRepository.update(id, { ...dto, updatedAt: new Date() }); const updatedUser = await this.userRepository.update(id, { ...dto, updatedAt: new Date().toISOString() });
return mapUserAdmin(updatedUser); return mapUserAdmin(updatedUser);
} }
@ -100,7 +100,7 @@ export class UserAdminService {
await this.albumRepository.softDeleteAll(id); await this.albumRepository.softDeleteAll(id);
const status = force ? UserStatus.REMOVING : UserStatus.DELETED; const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
const user = await this.userRepository.update(id, { status, deletedAt: new Date() }); const user = await this.userRepository.update(id, { status, deletedAt: new Date().toISOString() });
if (force) { if (force) {
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } }); await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });

View file

@ -24,7 +24,7 @@ import { Mocked } from 'vitest';
const makeDeletedAt = (daysAgo: number) => { const makeDeletedAt = (daysAgo: number) => {
const deletedAt = new Date(); const deletedAt = new Date();
deletedAt.setDate(deletedAt.getDate() - daysAgo); deletedAt.setDate(deletedAt.getDate() - daysAgo);
return deletedAt; return deletedAt.toISOString();
}; };
describe(UserService.name, () => { describe(UserService.name, () => {

View file

@ -179,7 +179,7 @@ export class UserService {
return false; return false;
} }
return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt); return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromISO(user.deletedAt);
} }
private async findOrFail(id: string, options: UserFindOptions) { private async findOrFail(id: string, options: UserFindOptions) {

View file

@ -7,8 +7,8 @@ export const sessionStub = {
token: 'auth_token', token: 'auth_token',
userId: userStub.user1.id, userId: userStub.user1.id,
user: userStub.user1, user: userStub.user1,
createdAt: new Date('2021-01-01'), createdAt: new Date('2021-01-01').toISOString(),
updatedAt: new Date(), updatedAt: new Date().toISOString(),
deviceType: '', deviceType: '',
deviceOS: '', deviceOS: '',
}), }),
@ -17,8 +17,8 @@ export const sessionStub = {
token: 'auth_token', token: 'auth_token',
userId: userStub.user1.id, userId: userStub.user1.id,
user: userStub.user1, user: userStub.user1,
createdAt: new Date('2021-01-01'), createdAt: new Date('2021-01-01').toISOString(),
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01').toISOString(),
deviceType: 'Mobile', deviceType: 'Mobile',
deviceOS: 'Android', deviceOS: 'Android',
}), }),