refactor: use zod codec for response DTO serialization

This commit is contained in:
timonrieger 2026-04-23 14:21:40 +02:00
parent 350056dd1a
commit fcd23ee043
No known key found for this signature in database
38 changed files with 286 additions and 196 deletions

View File

@ -157,10 +157,14 @@ class AlbumResponseDto {
json[r'albumUsers'] = this.albumUsers; json[r'albumUsers'] = this.albumUsers;
json[r'assetCount'] = this.assetCount; json[r'assetCount'] = this.assetCount;
json[r'contributorCounts'] = this.contributorCounts; json[r'contributorCounts'] = this.contributorCounts;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
json[r'description'] = this.description; json[r'description'] = this.description;
if (this.endDate != null) { if (this.endDate != null) {
json[r'endDate'] = this.endDate!.toUtc().toIso8601String(); json[r'endDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.endDate!.millisecondsSinceEpoch
: this.endDate!.toUtc().toIso8601String();
} else { } else {
// json[r'endDate'] = null; // json[r'endDate'] = null;
} }
@ -168,7 +172,9 @@ class AlbumResponseDto {
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isActivityEnabled'] = this.isActivityEnabled; json[r'isActivityEnabled'] = this.isActivityEnabled;
if (this.lastModifiedAssetTimestamp != null) { if (this.lastModifiedAssetTimestamp != null) {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); json[r'lastModifiedAssetTimestamp'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.lastModifiedAssetTimestamp!.millisecondsSinceEpoch
: this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else { } else {
// json[r'lastModifiedAssetTimestamp'] = null; // json[r'lastModifiedAssetTimestamp'] = null;
} }
@ -179,11 +185,15 @@ class AlbumResponseDto {
} }
json[r'shared'] = this.shared; json[r'shared'] = this.shared;
if (this.startDate != null) { if (this.startDate != null) {
json[r'startDate'] = this.startDate!.toUtc().toIso8601String(); json[r'startDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.startDate!.millisecondsSinceEpoch
: this.startDate!.toUtc().toIso8601String();
} else { } else {
// json[r'startDate'] = null; // json[r'startDate'] = null;
} }
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
return json; return json;
} }
@ -201,17 +211,17 @@ class AlbumResponseDto {
albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
assetCount: mapValueOfType<int>(json, r'assetCount')!, assetCount: mapValueOfType<int>(json, r'assetCount')!,
contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']),
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
description: mapValueOfType<String>(json, r'description')!, description: mapValueOfType<String>(json, r'description')!,
endDate: mapDateTime(json, r'endDate', r''), endDate: mapDateTime(json, r'endDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!, hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!, isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
order: AssetOrder.fromJson(json[r'order']), order: AssetOrder.fromJson(json[r'order']),
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,
startDate: mapDateTime(json, r'startDate', r''), startDate: mapDateTime(json, r'startDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
); );
} }
return null; return null;

View File

@ -246,7 +246,9 @@ class AssetResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'checksum'] = this.checksum; json[r'checksum'] = this.checksum;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
if (this.duplicateId != null) { if (this.duplicateId != null) {
json[r'duplicateId'] = this.duplicateId; json[r'duplicateId'] = this.duplicateId;
} else { } else {
@ -262,8 +264,12 @@ class AssetResponseDto {
} else { } else {
// json[r'exifInfo'] = null; // json[r'exifInfo'] = null;
} }
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); ? this.fileCreatedAt.millisecondsSinceEpoch
: this.fileCreatedAt.toUtc().toIso8601String();
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.fileModifiedAt.millisecondsSinceEpoch
: this.fileModifiedAt.toUtc().toIso8601String();
json[r'hasMetadata'] = this.hasMetadata; json[r'hasMetadata'] = this.hasMetadata;
if (this.height != null) { if (this.height != null) {
json[r'height'] = this.height; json[r'height'] = this.height;
@ -286,7 +292,9 @@ class AssetResponseDto {
} else { } else {
// json[r'livePhotoVideoId'] = null; // json[r'livePhotoVideoId'] = null;
} }
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String(); json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.localDateTime.millisecondsSinceEpoch
: this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName; json[r'originalFileName'] = this.originalFileName;
if (this.originalMimeType != null) { if (this.originalMimeType != null) {
json[r'originalMimeType'] = this.originalMimeType; json[r'originalMimeType'] = this.originalMimeType;
@ -319,7 +327,9 @@ class AssetResponseDto {
} }
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'unassignedFaces'] = this.unassignedFaces; json[r'unassignedFaces'] = this.unassignedFaces;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
json[r'visibility'] = this.visibility; json[r'visibility'] = this.visibility;
if (this.width != null) { if (this.width != null) {
json[r'width'] = this.width; json[r'width'] = this.width;
@ -339,12 +349,12 @@ class AssetResponseDto {
return AssetResponseDto( return AssetResponseDto(
checksum: mapValueOfType<String>(json, r'checksum')!, checksum: mapValueOfType<String>(json, r'checksum')!,
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
duplicateId: mapValueOfType<String>(json, r'duplicateId'), duplicateId: mapValueOfType<String>(json, r'duplicateId'),
duration: mapValueOfType<String>(json, r'duration'), duration: mapValueOfType<String>(json, r'duration'),
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!, hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
height: json[r'height'] == null height: json[r'height'] == null
? null ? null
@ -357,7 +367,7 @@ class AssetResponseDto {
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!, isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'')!, localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!, originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalMimeType: mapValueOfType<String>(json, r'originalMimeType'), originalMimeType: mapValueOfType<String>(json, r'originalMimeType'),
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
@ -370,7 +380,7 @@ class AssetResponseDto {
thumbhash: mapValueOfType<String>(json, r'thumbhash'), thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!, type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: json[r'width'] == null width: json[r'width'] == null
? null ? null

View File

@ -177,7 +177,9 @@ class ExifResponseDto {
// json[r'country'] = null; // json[r'country'] = null;
} }
if (this.dateTimeOriginal != null) { if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.dateTimeOriginal!.millisecondsSinceEpoch
: this.dateTimeOriginal!.toUtc().toIso8601String();
} else { } else {
// json[r'dateTimeOriginal'] = null; // json[r'dateTimeOriginal'] = null;
} }
@ -247,7 +249,9 @@ class ExifResponseDto {
// json[r'model'] = null; // json[r'model'] = null;
} }
if (this.modifyDate != null) { if (this.modifyDate != null) {
json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.modifyDate!.millisecondsSinceEpoch
: this.modifyDate!.toUtc().toIso8601String();
} else { } else {
// json[r'modifyDate'] = null; // json[r'modifyDate'] = null;
} }
@ -290,7 +294,7 @@ class ExifResponseDto {
return ExifResponseDto( return ExifResponseDto(
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
exifImageHeight: json[r'exifImageHeight'] == null exifImageHeight: json[r'exifImageHeight'] == null
? null ? null
@ -318,7 +322,7 @@ class ExifResponseDto {
: num.parse('${json[r'longitude']}'), : num.parse('${json[r'longitude']}'),
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
modifyDate: mapDateTime(json, r'modifyDate', r''), modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
orientation: mapValueOfType<String>(json, r'orientation'), orientation: mapValueOfType<String>(json, r'orientation'),
projectionType: mapValueOfType<String>(json, r'projectionType'), projectionType: mapValueOfType<String>(json, r'projectionType'),
rating: json[r'rating'] == null rating: json[r'rating'] == null

View File

@ -83,7 +83,9 @@ class PartnerResponseDto {
// json[r'inTimeline'] = null; // json[r'inTimeline'] = null;
} }
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
return json; return json;
} }
@ -102,7 +104,7 @@ class PartnerResponseDto {
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
inTimeline: mapValueOfType<bool>(json, r'inTimeline'), inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
); );
} }

View File

@ -94,7 +94,9 @@ class PersonResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.birthDate != null) { if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
? this.birthDate!.millisecondsSinceEpoch
: _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
@ -113,7 +115,9 @@ class PersonResponseDto {
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath; json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) { if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt!.millisecondsSinceEpoch
: this.updatedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'updatedAt'] = null; // json[r'updatedAt'] = null;
} }
@ -129,14 +133,14 @@ class PersonResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PersonResponseDto( return PersonResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!, thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''), updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
); );
} }
return null; return null;

View File

@ -99,7 +99,9 @@ class PersonWithFacesResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.birthDate != null) { if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
? this.birthDate!.millisecondsSinceEpoch
: _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
@ -119,7 +121,9 @@ class PersonWithFacesResponseDto {
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath; json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) { if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt!.millisecondsSinceEpoch
: this.updatedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'updatedAt'] = null; // json[r'updatedAt'] = null;
} }
@ -135,7 +139,7 @@ class PersonWithFacesResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PersonWithFacesResponseDto( return PersonWithFacesResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']), faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
@ -143,7 +147,7 @@ class PersonWithFacesResponseDto {
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!, thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''), updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
); );
} }
return null; return null;

View File

@ -86,7 +86,9 @@ class TagResponseDto {
} else { } else {
// json[r'color'] = null; // json[r'color'] = null;
} }
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
if (this.parentId != null) { if (this.parentId != null) {
@ -94,7 +96,9 @@ class TagResponseDto {
} else { } else {
// json[r'parentId'] = null; // json[r'parentId'] = null;
} }
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
json[r'value'] = this.value; json[r'value'] = this.value;
return json; return json;
} }
@ -109,11 +113,11 @@ class TagResponseDto {
return TagResponseDto( return TagResponseDto(
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
parentId: mapValueOfType<String>(json, r'parentId'), parentId: mapValueOfType<String>(json, r'parentId'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
value: mapValueOfType<String>(json, r'value')!, value: mapValueOfType<String>(json, r'value')!,
); );
} }

View File

@ -153,7 +153,9 @@ class UserAdminResponseDto {
} }
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId; json[r'oauthId'] = this.oauthId;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
if (this.quotaSizeInBytes != null) { if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
@ -196,7 +198,7 @@ class UserAdminResponseDto {
license: UserLicense.fromJson(json[r'license']), license: UserLicense.fromJson(json[r'license']),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!, oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'), quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'), quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),

View File

@ -66,7 +66,9 @@ class UserResponseDto {
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
return json; return json;
} }
@ -84,7 +86,7 @@ class UserResponseDto {
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
); );
} }

View File

@ -15300,7 +15300,9 @@
}, },
"createdAt": { "createdAt": {
"description": "Creation date", "description": "Creation date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"description": { "description": {
@ -15309,7 +15311,9 @@
}, },
"endDate": { "endDate": {
"description": "End date (latest asset)", "description": "End date (latest asset)",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"hasSharedLink": { "hasSharedLink": {
@ -15326,7 +15330,9 @@
}, },
"lastModifiedAssetTimestamp": { "lastModifiedAssetTimestamp": {
"description": "Last modified asset timestamp", "description": "Last modified asset timestamp",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"order": { "order": {
@ -15338,12 +15344,16 @@
}, },
"startDate": { "startDate": {
"description": "Start date (earliest asset)", "description": "Start date (earliest asset)",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
} }
}, },
@ -16618,7 +16628,9 @@
}, },
"createdAt": { "createdAt": {
"description": "The UTC timestamp when the asset was originally uploaded to Immich.", "description": "The UTC timestamp when the asset was originally uploaded to Immich.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"duplicateId": { "duplicateId": {
@ -16636,12 +16648,16 @@
}, },
"fileCreatedAt": { "fileCreatedAt": {
"description": "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.", "description": "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-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"fileModifiedAt": { "fileModifiedAt": {
"description": "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.", "description": "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-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"hasMetadata": { "hasMetadata": {
@ -16714,7 +16730,9 @@
}, },
"localDateTime": { "localDateTime": {
"description": "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.", "description": "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-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"originalFileName": { "originalFileName": {
@ -16787,7 +16805,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "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.", "description": "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-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"visibility": { "visibility": {
@ -17614,8 +17634,10 @@
"dateTimeOriginal": { "dateTimeOriginal": {
"default": null, "default": null,
"description": "Original date/time", "description": "Original date/time",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"description": { "description": {
@ -17703,8 +17725,10 @@
"modifyDate": { "modifyDate": {
"default": null, "default": null,
"description": "Modification date/time", "description": "Modification date/time",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"orientation": { "orientation": {
@ -19245,7 +19269,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {
@ -19600,8 +19626,10 @@
"properties": { "properties": {
"birthDate": { "birthDate": {
"description": "Person date of birth", "description": "Person date of birth",
"example": "2024-01-01",
"format": "date", "format": "date",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string" "type": "string"
}, },
"color": { "color": {
@ -19652,7 +19680,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string", "type": "string",
"x-immich-history": [ "x-immich-history": [
{ {
@ -19729,8 +19759,10 @@
"properties": { "properties": {
"birthDate": { "birthDate": {
"description": "Person date of birth", "description": "Person date of birth",
"example": "2024-01-01",
"format": "date", "format": "date",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string" "type": "string"
}, },
"color": { "color": {
@ -19787,7 +19819,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string", "type": "string",
"x-immich-history": [ "x-immich-history": [
{ {
@ -24814,7 +24848,9 @@
}, },
"createdAt": { "createdAt": {
"description": "Creation date", "description": "Creation date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"id": { "id": {
@ -24831,7 +24867,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"value": { "value": {
@ -25485,7 +25523,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {
@ -25767,7 +25807,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {

View File

@ -42,19 +42,19 @@ import { configureUserAgent } from 'src/utils/fetch';
const common = [...repositories, ...services, GlobalExceptionFilter]; const common = [...repositories, ...services, GlobalExceptionFilter];
const configRepository = new ConfigRepository();
const { bull, cls, database, otel } = configRepository.getEnv();
const commonMiddleware = [ const commonMiddleware = [
{ provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_FILTER, useClass: GlobalExceptionFilter },
{ provide: APP_PIPE, useClass: ZodValidationPipe }, { provide: APP_PIPE, useClass: ZodValidationPipe },
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, ...(configRepository.isDev() ? [{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }] : []),
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
]; ];
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, { provide: APP_GUARD, useClass: AuthGuard }]; const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, { provide: APP_GUARD, useClass: AuthGuard }];
const configRepository = new ConfigRepository();
const { bull, cls, database, otel } = configRepository.getEnv();
const commonImports = [ const commonImports = [
ClsModule.forRoot(cls.config), ClsModule.forRoot(cls.config),
KyselyModule.forRoot(getKyselyConfig(database.config)), KyselyModule.forRoot(getKyselyConfig(database.config)),

View File

@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AddUsersDto, AddUsersDto,
@ -27,6 +28,7 @@ export class AlbumController {
@Get() @Get()
@Authenticated({ permission: Permission.AlbumRead }) @Authenticated({ permission: Permission.AlbumRead })
@ZodSerializerDto([AlbumResponseDto])
@Endpoint({ @Endpoint({
summary: 'List all albums', summary: 'List all albums',
description: 'Retrieve a list of albums available to the authenticated user.', description: 'Retrieve a list of albums available to the authenticated user.',
@ -38,6 +40,7 @@ export class AlbumController {
@Post() @Post()
@Authenticated({ permission: Permission.AlbumCreate }) @Authenticated({ permission: Permission.AlbumCreate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create an album', summary: 'Create an album',
description: 'Create a new album. The album can also be created with initial users and assets.', description: 'Create a new album. The album can also be created with initial users and assets.',
@ -60,6 +63,7 @@ export class AlbumController {
@Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) @Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
@Get(':id') @Get(':id')
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve an album', summary: 'Retrieve an album',
description: 'Retrieve information about a specific album by its ID.', description: 'Retrieve information about a specific album by its ID.',
@ -71,6 +75,7 @@ export class AlbumController {
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.AlbumUpdate }) @Authenticated({ permission: Permission.AlbumUpdate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update an album', summary: 'Update an album',
description: description:
@ -152,6 +157,7 @@ export class AlbumController {
@Put(':id/users') @Put(':id/users')
@Authenticated({ permission: Permission.AlbumUserCreate }) @Authenticated({ permission: Permission.AlbumUserCreate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Share album with users', summary: 'Share album with users',
description: 'Share an album with multiple users. Each user can be given a specific role in the album.', description: 'Share an album with multiple users. Each user can be given a specific role in the album.',

View File

@ -1,7 +1,10 @@
import { AssetController } from 'src/controllers/asset.controller'; import { AssetController } from 'src/controllers/asset.controller';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetMetadataKey } from 'src/enum'; import { AssetMetadataKey } from 'src/enum';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import request from 'supertest'; import request from 'supertest';
import { AssetFactory } from 'test/factories/asset.factory';
import { getForAsset } from 'test/mappers';
import { factory } from 'test/small.factory'; import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
@ -183,6 +186,10 @@ describe(AssetController.name, () => {
}); });
describe('PUT /assets/:id', () => { describe('PUT /assets/:id', () => {
beforeEach(() => {
service.update.mockResolvedValue(mapAsset(getForAsset(AssetFactory.create())));
});
it('should be an authenticated route', async () => { it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/123`); await request(ctx.getHttpServer()).get(`/assets/123`);
expect(ctx.authenticate).toHaveBeenCalled(); expect(ctx.authenticate).toHaveBeenCalled();

View File

@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { import {
@ -79,6 +80,7 @@ export class AssetController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ZodSerializerDto(AssetResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve an asset', summary: 'Retrieve an asset',
description: 'Retrieve detailed information about a specific asset.', description: 'Retrieve detailed information about a specific asset.',
@ -128,6 +130,7 @@ export class AssetController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@ZodSerializerDto(AssetResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update an asset', summary: 'Update an asset',
description: 'Update information of a specific asset.', description: 'Update information of a specific asset.',

View File

@ -1,7 +1,9 @@
import { AuthController } from 'src/controllers/auth.controller'; import { AuthController } from 'src/controllers/auth.controller';
import { LoginResponseDto } from 'src/dtos/auth.dto'; import { LoginResponseDto } from 'src/dtos/auth.dto';
import { mapUserAdmin } from 'src/dtos/user.dto';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import request from 'supertest'; import request from 'supertest';
import { UserFactory } from 'test/factories/user.factory';
import { mediumFactory } from 'test/medium.factory'; import { mediumFactory } from 'test/medium.factory';
import { errorDto } from 'test/medium/responses'; import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
@ -53,6 +55,7 @@ describe(AuthController.name, () => {
it('should transform email to lower case', async () => { it('should transform email to lower case', async () => {
service.adminSignUp.mockReset(); service.adminSignUp.mockReset();
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
const { status } = await request(ctx.getHttpServer()) const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
@ -61,6 +64,7 @@ describe(AuthController.name, () => {
}); });
it('should accept an email with a local domain', async () => { it('should accept an email with a local domain', async () => {
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
const { status } = await request(ctx.getHttpServer()) const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'admin@local' }); .send({ name: 'admin', password: 'password', email: 'admin@local' });

View File

@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AuthDto, AuthDto,
@ -50,6 +51,7 @@ export class AuthController {
} }
@Post('admin-sign-up') @Post('admin-sign-up')
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Register admin', summary: 'Register admin',
description: 'Create the first admin user in the system.', description: 'Create the first admin user in the system.',
@ -74,6 +76,7 @@ export class AuthController {
@Post('change-password') @Post('change-password')
@Authenticated({ permission: Permission.AuthChangePassword }) @Authenticated({ permission: Permission.AuthChangePassword })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Change password', summary: 'Change password',
description: 'Change the password of the current user.', description: 'Change the password of the current user.',

View File

@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@ -44,6 +45,7 @@ export class FaceController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.FaceUpdate }) @Authenticated({ permission: Permission.FaceUpdate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Re-assign a face to another person', summary: 'Re-assign a face to another person',
description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.', description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.',

View File

@ -1,6 +1,7 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AuthDto, AuthDto,
@ -89,6 +90,7 @@ export class OAuthController {
@Post('link') @Post('link')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Link OAuth account', summary: 'Link OAuth account',
description: 'Link an OAuth account to the authenticated user.', description: 'Link an OAuth account to the authenticated user.',
@ -105,6 +107,7 @@ export class OAuthController {
@Post('unlink') @Post('unlink')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Unlink OAuth account', summary: 'Unlink OAuth account',
description: 'Unlink the OAuth account from the authenticated user.', description: 'Unlink the OAuth account from the authenticated user.',

View File

@ -14,6 +14,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -58,6 +59,7 @@ export class PersonController {
@Post() @Post()
@Authenticated({ permission: Permission.PersonCreate }) @Authenticated({ permission: Permission.PersonCreate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a person', summary: 'Create a person',
description: 'Create a new person that can have multiple faces assigned to them.', description: 'Create a new person that can have multiple faces assigned to them.',
@ -92,6 +94,7 @@ export class PersonController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Get a person', summary: 'Get a person',
description: 'Retrieve a person by id.', description: 'Retrieve a person by id.',
@ -103,6 +106,7 @@ export class PersonController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PersonUpdate }) @Authenticated({ permission: Permission.PersonUpdate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update person', summary: 'Update person',
description: 'Update an individual person.', description: 'Update an individual person.',
@ -158,6 +162,7 @@ export class PersonController {
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated({ permission: Permission.PersonReassign }) @Authenticated({ permission: Permission.PersonReassign })
@ZodSerializerDto([PersonResponseDto])
@Endpoint({ @Endpoint({
summary: 'Reassign faces', summary: 'Reassign faces',
description: 'Bulk reassign a list of faces to a different person.', description: 'Bulk reassign a list of faces to a different person.',

View File

@ -1,5 +1,6 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -54,6 +55,7 @@ export class SearchController {
@Post('random') @Post('random')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search random assets', summary: 'Search random assets',
description: 'Retrieve a random selection of assets based on the provided criteria.', description: 'Retrieve a random selection of assets based on the provided criteria.',
@ -66,6 +68,7 @@ export class SearchController {
@Post('large-assets') @Post('large-assets')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search large assets', summary: 'Search large assets',
description: 'Search for assets that are considered large based on specified criteria.', description: 'Search for assets that are considered large based on specified criteria.',
@ -100,6 +103,7 @@ export class SearchController {
@Get('person') @Get('person')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ZodSerializerDto([PersonResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search people', summary: 'Search people',
description: 'Search for people by name.', description: 'Search for people by name.',
@ -122,6 +126,7 @@ export class SearchController {
@Get('cities') @Get('cities')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve assets by city', summary: 'Retrieve assets by city',
description: description:

View File

@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -23,6 +24,7 @@ export class TagController {
@Post() @Post()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a tag', summary: 'Create a tag',
description: 'Create a new tag by providing a name and optional color.', description: 'Create a new tag by providing a name and optional color.',
@ -34,6 +36,7 @@ export class TagController {
@Get() @Get()
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ZodSerializerDto([TagResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve tags', summary: 'Retrieve tags',
description: 'Retrieve a list of all tags.', description: 'Retrieve a list of all tags.',
@ -45,6 +48,7 @@ export class TagController {
@Put() @Put()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ZodSerializerDto([TagResponseDto])
@Endpoint({ @Endpoint({
summary: 'Upsert tags', summary: 'Upsert tags',
description: 'Create or update multiple tags in a single request.', description: 'Create or update multiple tags in a single request.',
@ -67,6 +71,7 @@ export class TagController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a tag', summary: 'Retrieve a tag',
description: 'Retrieve a specific tag by its ID.', description: 'Retrieve a specific tag by its ID.',
@ -78,6 +83,7 @@ export class TagController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.TagUpdate }) @Authenticated({ permission: Permission.TagUpdate })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update a tag', summary: 'Update a tag',
description: 'Update an existing tag identified by its ID.', description: 'Update an existing tag identified by its ID.',

View File

@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -24,6 +25,7 @@ export class UserAdminController {
@Get() @Get()
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ZodSerializerDto([UserAdminResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search users', summary: 'Search users',
description: 'Search for users.', description: 'Search for users.',
@ -35,6 +37,7 @@ export class UserAdminController {
@Post() @Post()
@Authenticated({ permission: Permission.AdminUserCreate, admin: true }) @Authenticated({ permission: Permission.AdminUserCreate, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a user', summary: 'Create a user',
description: 'Create a new user.', description: 'Create a new user.',
@ -46,6 +49,7 @@ export class UserAdminController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a user', summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.', description: 'Retrieve a specific user by their ID.',
@ -57,6 +61,7 @@ export class UserAdminController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update a user', summary: 'Update a user',
description: 'Update an existing user.', description: 'Update an existing user.',
@ -72,6 +77,7 @@ export class UserAdminController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Delete a user', summary: 'Delete a user',
description: 'Delete a user.', description: 'Delete a user.',
@ -140,6 +146,7 @@ export class UserAdminController {
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Restore a deleted user', summary: 'Restore a deleted user',
description: 'Restore a previously deleted user.', description: 'Restore a previously deleted user.',

View File

@ -15,6 +15,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@ -40,6 +41,7 @@ export class UserController {
@Get() @Get()
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto([UserResponseDto])
@Endpoint({ @Endpoint({
summary: 'Get all users', summary: 'Get all users',
description: 'Retrieve a list of all users on the server.', description: 'Retrieve a list of all users on the server.',
@ -51,6 +53,7 @@ export class UserController {
@Get('me') @Get('me')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Get current user', summary: 'Get current user',
description: 'Retrieve information about the user making the API request.', description: 'Retrieve information about the user making the API request.',
@ -62,6 +65,7 @@ export class UserController {
@Put('me') @Put('me')
@Authenticated({ permission: Permission.UserUpdate }) @Authenticated({ permission: Permission.UserUpdate })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update current user', summary: 'Update current user',
description: 'Update the current user making the API request.', description: 'Update the current user making the API request.',
@ -166,6 +170,7 @@ export class UserController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto(UserResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a user', summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.', description: 'Retrieve a specific user by their ID.',

View File

@ -1,5 +1,6 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -25,6 +26,7 @@ export class ViewController {
@Get('folder') @Get('folder')
@Authenticated({ permission: Permission.FolderRead }) @Authenticated({ permission: Permission.FolderRead })
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve assets by original path', summary: 'Retrieve assets by original path',
description: 'Retrieve assets that are children of a specific folder.', description: 'Retrieve assets that are children of a specific folder.',

View File

@ -11,8 +11,8 @@ describe('mapAlbum', () => {
.asset({ localDateTime: startDate }, (builder) => builder.exif()) .asset({ localDateTime: startDate }, (builder) => builder.exif())
.build(); .build();
const dto = mapAlbum(getForAlbum(album)); const dto = mapAlbum(getForAlbum(album));
expect(dto.startDate).toEqual(startDate.toISOString()); expect(dto.startDate).toEqual(startDate);
expect(dto.endDate).toEqual(endDate.toISOString()); expect(dto.endDate).toEqual(endDate);
}); });
it('should not set start and end dates for empty assets', () => { it('should not set start and end dates for empty assets', () => {

View File

@ -6,8 +6,7 @@ import { MapAsset } from 'src/dtos/asset-response.dto';
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { isoDatetimeToDate, stringToBool } from 'src/validation';
import { stringToBool } from 'src/validation';
import z from 'zod'; import z from 'zod';
const AlbumUserAddSchema = z const AlbumUserAddSchema = z
@ -105,10 +104,8 @@ export const AlbumResponseSchema = z
id: z.string().describe('Album ID'), id: z.string().describe('Album ID'),
albumName: z.string().describe('Album name'), albumName: z.string().describe('Album name'),
description: z.string().describe('Album description'), description: z.string().describe('Album description'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('Creation date'),
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), updatedAt: isoDatetimeToDate.describe('Last update date'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
shared: z.boolean().describe('Is shared album'), shared: z.boolean().describe('Is shared album'),
albumUsers: z albumUsers: z
@ -119,16 +116,9 @@ export const AlbumResponseSchema = z
), ),
hasSharedLink: z.boolean().describe('Has shared link'), hasSharedLink: z.boolean().describe('Has shared link'),
assetCount: z.int().min(0).describe('Number of assets'), assetCount: z.int().min(0).describe('Number of assets'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. lastModifiedAssetTimestamp: isoDatetimeToDate.optional().describe('Last modified asset timestamp'),
lastModifiedAssetTimestamp: z startDate: isoDatetimeToDate.optional().describe('Start date (earliest asset)'),
.string() endDate: isoDatetimeToDate.optional().describe('End date (latest asset)'),
.meta({ format: 'date-time' })
.optional()
.describe('Last modified asset timestamp'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'),
isActivityEnabled: z.boolean().describe('Activity feed enabled'), isActivityEnabled: z.boolean().describe('Activity feed enabled'),
order: AssetOrderSchema.optional(), order: AssetOrderSchema.optional(),
contributorCounts: z.array(ContributorCountResponseSchema).optional(), contributorCounts: z.array(ContributorCountResponseSchema).optional(),
@ -144,7 +134,7 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} export class AlbumResponseDto extends createZodDto(AlbumResponseSchema, { codec: true }) {}
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
export type MapAlbumDto = { export type MapAlbumDto = {
@ -190,14 +180,14 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
albumName: entity.albumName, albumName: entity.albumName,
description: entity.description, description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId, albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
id: entity.id, id: entity.id,
albumUsers, albumUsers,
shared: hasSharedUser || hasSharedLink, shared: hasSharedUser || hasSharedLink,
hasSharedLink, hasSharedLink,
startDate: asDateString(startDate), startDate: startDate ? new Date(startDate) : undefined,
endDate: asDateString(endDate), endDate: endDate ? new Date(endDate) : undefined,
assetCount: entity.assets?.length || 0, assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled, isActivityEnabled: entity.isActivityEnabled,
order: entity.order, order: entity.order,

View File

@ -25,8 +25,8 @@ import {
import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { getDimensions } from 'src/utils/asset.util'; import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { asDateString } from 'src/utils/date';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { isoDatetimeToDate } from 'src/validation';
import z from 'zod'; import z from 'zod';
const SanitizedAssetResponseSchema = z const SanitizedAssetResponseSchema = z
@ -40,13 +40,9 @@ const SanitizedAssetResponseSchema = z
) )
.nullable(), .nullable(),
originalMimeType: z.string().optional().describe('Original MIME type'), originalMimeType: z.string().optional().describe('Original MIME type'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. localDateTime: isoDatetimeToDate.describe(
localDateTime: z '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.',
.string() ),
.meta({ format: 'date-time' })
.describe(
'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.',
),
duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'), duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'),
livePhotoVideoId: z.string().nullish().describe('Live photo video ID'), livePhotoVideoId: z.string().nullish().describe('Live photo video ID'),
hasMetadata: z.boolean().describe('Whether asset has metadata'), hasMetadata: z.boolean().describe('Whether asset has metadata'),
@ -55,7 +51,7 @@ const SanitizedAssetResponseSchema = z
}) })
.meta({ id: 'SanitizedAssetResponseDto' }); .meta({ id: 'SanitizedAssetResponseDto' });
export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema, { codec: true }) {}
const AssetStackResponseSchema = z const AssetStackResponseSchema = z
.object({ .object({
@ -67,11 +63,7 @@ const AssetStackResponseSchema = z
export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
z.object({ z.object({
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
createdAt: z
.string()
.meta({ format: 'date-time' })
.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
ownerId: z.string().describe('Owner user ID'), ownerId: z.string().describe('Owner user ID'),
owner: UserResponseSchema.optional(), owner: UserResponseSchema.optional(),
libraryId: z libraryId: z
@ -81,25 +73,15 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
.meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()), .meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()),
originalPath: z.string().describe('Original file path'), originalPath: z.string().describe('Original file path'),
originalFileName: z.string().describe('Original file name'), originalFileName: z.string().describe('Original file name'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. fileCreatedAt: isoDatetimeToDate.describe(
fileCreatedAt: z '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.',
.string() ),
.meta({ format: 'date-time' }) fileModifiedAt: isoDatetimeToDate.describe(
.describe( '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.',
'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.', ),
), updatedAt: isoDatetimeToDate.describe(
fileModifiedAt: z '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.',
.string() ),
.meta({ format: 'date-time' })
.describe(
'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.',
),
updatedAt: z
.string()
.meta({ format: 'date-time' })
.describe(
'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.',
),
isFavorite: z.boolean().describe('Is favorite'), isFavorite: z.boolean().describe('Is favorite'),
isArchived: z.boolean().describe('Is archived'), isArchived: z.boolean().describe('Is archived'),
isTrashed: z.boolean().describe('Is trashed'), isTrashed: z.boolean().describe('Is trashed'),
@ -124,7 +106,7 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
}).shape, }).shape,
).meta({ id: 'AssetResponseDto' }); ).meta({ id: 'AssetResponseDto' });
export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export class AssetResponseDto extends createZodDto(AssetResponseSchema, { codec: true }) {}
export type MapAsset = { export type MapAsset = {
createdAt: Date; createdAt: Date;
@ -220,7 +202,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
type: entity.type, type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
localDateTime: asDateString(entity.localDateTime), localDateTime: new Date(entity.localDateTime),
duration: entity.duration, duration: entity.duration,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false, hasMetadata: false,
@ -234,7 +216,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
return { return {
id: entity.id, id: entity.id,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
ownerId: entity.ownerId, ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined, owner: entity.owner ? mapUser(entity.owner) : undefined,
libraryId: entity.libraryId, libraryId: entity.libraryId,
@ -243,10 +225,10 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
fileCreatedAt: asDateString(entity.fileCreatedAt), fileCreatedAt: new Date(entity.fileCreatedAt),
fileModifiedAt: asDateString(entity.fileModifiedAt), fileModifiedAt: new Date(entity.fileModifiedAt),
localDateTime: asDateString(entity.localDateTime), localDateTime: new Date(entity.localDateTime),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite, isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
isArchived: entity.visibility === AssetVisibility.Archive, isArchived: entity.visibility === AssetVisibility.Archive,
isTrashed: !!entity.deletedAt, isTrashed: !!entity.deletedAt,

View File

@ -21,7 +21,10 @@ export type AuthDto = {
const LoginCredentialSchema = z const LoginCredentialSchema = z
.object({ .object({
email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), email: toEmail
.transform((val) => val.toLowerCase())
.describe('User email')
.meta({ example: 'testuser@email.com' }),
password: z.string().describe('User password').meta({ example: 'password' }), password: z.string().describe('User password').meta({ example: 'password' }),
}) })
.meta({ id: 'LoginCredentialDto' }); .meta({ id: 'LoginCredentialDto' });

View File

@ -1,7 +1,7 @@
import { createZodDto } from 'nestjs-zod'; import { createZodDto } from 'nestjs-zod';
import { Exif } from 'src/database'; import { Exif } from 'src/database';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { isoDatetimeToDate } from 'src/validation';
import z from 'zod'; import z from 'zod';
export const ExifResponseSchema = z export const ExifResponseSchema = z
@ -12,10 +12,8 @@ export const ExifResponseSchema = z
exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'), exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'),
fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'), fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'),
orientation: z.string().nullish().default(null).describe('Image orientation'), orientation: z.string().nullish().default(null).describe('Image orientation'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. dateTimeOriginal: isoDatetimeToDate.nullish().default(null).describe('Original date/time'),
dateTimeOriginal: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Original date/time'), modifyDate: isoDatetimeToDate.nullish().default(null).describe('Modification date/time'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'),
timeZone: z.string().nullish().default(null).describe('Time zone'), timeZone: z.string().nullish().default(null).describe('Time zone'),
lensModel: z.string().nullish().default(null).describe('Lens model'), lensModel: z.string().nullish().default(null).describe('Lens model'),
fNumber: z.number().nullish().default(null).describe('F-number (aperture)'), fNumber: z.number().nullish().default(null).describe('F-number (aperture)'),
@ -34,7 +32,7 @@ export const ExifResponseSchema = z
.describe('EXIF response') .describe('EXIF response')
.meta({ id: 'ExifResponseDto' }); .meta({ id: 'ExifResponseDto' });
class ExifResponseDto extends createZodDto(ExifResponseSchema) {} class ExifResponseDto extends createZodDto(ExifResponseSchema, { codec: true }) {}
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto { export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
return { return {
@ -44,8 +42,8 @@ export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
exifImageHeight: entity.exifImageHeight, exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation, orientation: entity.orientation,
dateTimeOriginal: asDateString(entity.dateTimeOriginal), dateTimeOriginal: entity.dateTimeOriginal ? new Date(entity.dateTimeOriginal) : null,
modifyDate: asDateString(entity.modifyDate), modifyDate: entity.modifyDate ? new Date(entity.modifyDate) : null,
timeZone: entity.timeZone, timeZone: entity.timeZone,
lensModel: entity.lensModel, lensModel: entity.lensModel,
fNumber: entity.fNumber, fNumber: entity.fNumber,

View File

@ -7,9 +7,8 @@ import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { SourceTypeSchema } from 'src/enum'; import { SourceTypeSchema } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { asBirthDateString, asDateString } from 'src/utils/date';
import { transformFaceBoundingBox } from 'src/utils/transform'; import { transformFaceBoundingBox } from 'src/utils/transform';
import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; import { emptyStringToNull, hexColor, isoDateToDate, isoDatetimeToDate, stringToBool } from 'src/validation';
import z from 'zod'; import z from 'zod';
const PersonCreateSchema = z const PersonCreateSchema = z
@ -60,14 +59,10 @@ const PersonResponseSchema = z
.object({ .object({
id: z.string().describe('Person ID'), id: z.string().describe('Person ID'),
name: z.string().describe('Person name'), name: z.string().describe('Person name'),
// TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers. birthDate: isoDateToDate.nullable().describe('Person date of birth'),
birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(),
thumbnailPath: z.string().describe('Thumbnail path'), thumbnailPath: z.string().describe('Thumbnail path'),
isHidden: z.boolean().describe('Is hidden'), isHidden: z.boolean().describe('Is hidden'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. updatedAt: isoDatetimeToDate
updatedAt: z
.string()
.meta({ format: 'date-time' })
.optional() .optional()
.describe('Last update date') .describe('Last update date')
.meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()), .meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()),
@ -89,7 +84,7 @@ export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
export class MergePersonDto extends createZodDto(MergePersonSchema) {} export class MergePersonDto extends createZodDto(MergePersonSchema) {}
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} export class PersonResponseDto extends createZodDto(PersonResponseSchema, { codec: true }) {}
export const AssetFaceWithoutPersonResponseSchema = z export const AssetFaceWithoutPersonResponseSchema = z
.object({ .object({
@ -111,7 +106,7 @@ export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({
faces: z.array(AssetFaceWithoutPersonResponseSchema), faces: z.array(AssetFaceWithoutPersonResponseSchema),
}).meta({ id: 'PersonWithFacesResponseDto' }); }).meta({ id: 'PersonWithFacesResponseDto' });
export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema, { codec: true }) {}
const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({
person: PersonResponseSchema.nullable(), person: PersonResponseSchema.nullable(),
@ -184,12 +179,12 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
return { return {
id: person.id, id: person.id,
name: person.name, name: person.name,
birthDate: asBirthDateString(person.birthDate), birthDate: person.birthDate ? new Date(person.birthDate) : null,
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: person.color ?? undefined, color: person.color ?? undefined,
updatedAt: asDateString(person.updatedAt), updatedAt: person.updatedAt ? new Date(person.updatedAt) : undefined,
}; };
} }

View File

@ -1,8 +1,7 @@
import { createZodDto } from 'nestjs-zod'; import { createZodDto } from 'nestjs-zod';
import { Tag } from 'src/database'; import { Tag } from 'src/database';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { emptyStringToNull, hexColor, isoDatetimeToDate } from 'src/validation';
import { emptyStringToNull, hexColor } from 'src/validation';
import z from 'zod'; import z from 'zod';
const TagCreateSchema = z const TagCreateSchema = z
@ -44,10 +43,8 @@ export const TagResponseSchema = z
parentId: z.string().optional().describe('Parent tag ID'), parentId: z.string().optional().describe('Parent tag ID'),
name: z.string().describe('Tag name'), name: z.string().describe('Tag name'),
value: z.string().describe('Tag value (full path)'), value: z.string().describe('Tag value (full path)'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('Creation date'),
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), updatedAt: isoDatetimeToDate.describe('Last update date'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
color: z.string().optional().describe('Tag color (hex)'), color: z.string().optional().describe('Tag color (hex)'),
}) })
.meta({ id: 'TagResponseDto' }); .meta({ id: 'TagResponseDto' });
@ -57,7 +54,7 @@ export class TagUpdateDto extends createZodDto(TagUpdateSchema) {}
export class TagUpsertDto extends createZodDto(TagUpsertSchema) {} export class TagUpsertDto extends createZodDto(TagUpsertSchema) {}
export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {} export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {}
export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {} export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {}
export class TagResponseDto extends createZodDto(TagResponseSchema) {} export class TagResponseDto extends createZodDto(TagResponseSchema, { codec: true }) {}
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto { export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
return { return {
@ -65,8 +62,8 @@ export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
parentId: entity.parentId ?? undefined, parentId: entity.parentId ?? undefined,
name: entity.value.split('/').at(-1) as string, name: entity.value.split('/').at(-1) as string,
value: entity.value, value: entity.value,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
color: entity.color ?? undefined, color: entity.color ?? undefined,
}; };
} }

View File

@ -3,13 +3,15 @@ import { User, UserAdmin } from 'src/database';
import { pinCodeRegex } from 'src/dtos/auth.dto'; import { pinCodeRegex } from 'src/dtos/auth.dto';
import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum';
import { MaybeDehydrated, UserMetadataItem } from 'src/types'; import { MaybeDehydrated, UserMetadataItem } from 'src/types';
import { asDateString } from 'src/utils/date';
import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
import z from 'zod'; import z from 'zod';
export const UserUpdateMeSchema = z export const UserUpdateMeSchema = z
.object({ .object({
email: toEmail.optional().describe('User email'), email: toEmail
.transform((val) => val.toLowerCase())
.optional()
.describe('User email'),
password: z password: z
.string() .string()
.optional() .optional()
@ -29,12 +31,11 @@ export const UserResponseSchema = z
email: toEmail.describe('User email'), email: toEmail.describe('User email'),
profileImagePath: z.string().describe('Profile image path'), profileImagePath: z.string().describe('Profile image path'),
avatarColor: UserAvatarColorSchema, avatarColor: UserAvatarColorSchema,
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. profileChangedAt: isoDatetimeToDate.describe('Profile change date'),
profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'),
}) })
.meta({ id: 'UserResponseDto' }); .meta({ id: 'UserResponseDto' });
export class UserResponseDto extends createZodDto(UserResponseSchema) {} export class UserResponseDto extends createZodDto(UserResponseSchema, { codec: true }) {}
const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/;
@ -61,7 +62,7 @@ export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponse
name: entity.name, name: entity.name,
profileImagePath: entity.profileImagePath, profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email), avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: asDateString(entity.profileChangedAt), profileChangedAt: new Date(entity.profileChangedAt),
}; };
}; };
@ -76,7 +77,7 @@ export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {}
export const UserAdminCreateSchema = z export const UserAdminCreateSchema = z
.object({ .object({
email: toEmail.describe('User email'), email: toEmail.transform((val) => val.toLowerCase()).describe('User email'),
password: z.string().describe('User password'), password: z.string().describe('User password'),
name: z.string().describe('User name'), name: z.string().describe('User name'),
avatarColor: UserAvatarColorSchema.nullish(), avatarColor: UserAvatarColorSchema.nullish(),
@ -96,7 +97,10 @@ export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {}
const UserAdminUpdateSchema = z const UserAdminUpdateSchema = z
.object({ .object({
email: toEmail.optional().describe('User email'), email: toEmail
.transform((val) => val.toLowerCase())
.optional()
.describe('User email'),
password: z.string().optional().describe('User password'), password: z.string().optional().describe('User password'),
pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable())
.optional() .optional()
@ -135,7 +139,7 @@ const UserAdminResponseSchema = UserResponseSchema.extend({
license: UserLicenseSchema.nullable(), license: UserLicenseSchema.nullable(),
}).meta({ id: 'UserAdminResponseDto' }); }).meta({ id: 'UserAdminResponseDto' });
export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema, { codec: true }) {}
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
const metadata = entity.metadata || []; const metadata = entity.metadata || [];

View File

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

View File

@ -211,11 +211,12 @@ describe(PersonService.name, () => {
await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({ await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({
id: person.id, id: person.id,
name: person.name, name: person.name,
birthDate: '1976-06-30', birthDate: new Date('1976-06-30'),
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: false, isHidden: false,
isFavorite: false, isFavorite: false,
updatedAt: expect.any(String), color: undefined,
updatedAt: expect.any(Date),
}); });
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' }); expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' });
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
@ -485,10 +486,11 @@ describe(PersonService.name, () => {
birthDate: person.birthDate, birthDate: person.birthDate,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: undefined,
id: person.id, id: person.id,
name: person.name, name: person.name,
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
updatedAt: expect.any(String), updatedAt: expect.any(Date),
}); });
expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queue).not.toHaveBeenCalledWith();
@ -848,7 +850,7 @@ describe(PersonService.name, () => {
facesRecognizedAt: expect.any(Date), facesRecognizedAt: expect.any(Date),
}); });
const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date; const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date;
expect(facesRecognizedAt.getTime()).toBeGreaterThan(start); expect(facesRecognizedAt.getTime()).toBeGreaterThanOrEqual(start);
}); });
it('should create a face with no person and queue recognition job', async () => { it('should create a face with no person and queue recognition job', async () => {

View File

@ -1,25 +1,5 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
/**
* 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>);
};
/**
* 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.
*/
export const asBirthDateString = (x: Date | string | null): string | null => {
return x instanceof Date ? x.toISOString().split('T')[0] : x;
};
export const extractTimeZone = (dateTimeOriginal?: string | null) => { export const extractTimeZone = (dateTimeOriginal?: string | null) => {
const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined;
return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined; return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined;

View File

@ -5,7 +5,7 @@ import { getExifCount, suggestDuplicate, suggestDuplicateKeepAssetIds } from 'sr
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { z } from 'zod'; import type { z } from 'zod';
type ExifInfoInput = Partial<z.infer<typeof ExifResponseSchema>>; type ExifInfoInput = Partial<z.input<typeof ExifResponseSchema>>;
const createAsset = ( const createAsset = (
id: string, id: string,
@ -15,18 +15,18 @@ const createAsset = (
id, id,
type: AssetType.Image, type: AssetType.Image,
thumbhash: null, thumbhash: null,
localDateTime: new Date().toISOString(), localDateTime: new Date(),
duration: '0:00:00.00000', duration: '0:00:00.00000',
hasMetadata: true, hasMetadata: true,
width: 1920, width: 1920,
height: 1080, height: 1080,
createdAt: new Date().toISOString(), createdAt: new Date(),
ownerId: 'owner-1', ownerId: 'owner-1',
originalPath: '/path/to/asset', originalPath: '/path/to/asset',
originalFileName: 'asset.jpg', originalFileName: 'asset.jpg',
fileCreatedAt: new Date().toISOString(), fileCreatedAt: new Date(),
fileModifiedAt: new Date().toISOString(), fileModifiedAt: new Date(),
updatedAt: new Date().toISOString(), updatedAt: new Date(),
isFavorite: false, isFavorite: false,
isArchived: false, isArchived: false,
isTrashed: false, isTrashed: false,

View File

@ -135,15 +135,13 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num
* Converts email strings to lowercase and validates against HTML5 email regex * Converts email strings to lowercase and validates against HTML5 email regex
* @docs https://zod.dev/api?id=email * @docs https://zod.dev/api?id=email
*/ */
export const toEmail = z export const toEmail = z.email({
.email({ pattern: z.regexes.html5Email,
pattern: z.regexes.html5Email, error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`,
error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`, });
})
.transform((val) => val.toLowerCase());
/** /**
* Parse ISO 8601 datetime strings to Date objects * Parse ISO 8601 datetime strings to Date objects. Requires `{ codec: true }` when using `createZodDto`.
* @docs https://zod.dev/api?id=codec * @docs https://zod.dev/api?id=codec
*/ */
export const isoDatetimeToDate = z export const isoDatetimeToDate = z

View File

@ -55,15 +55,15 @@ export const tagStub = {
export const tagResponseStub = { export const tagResponseStub = {
tag1: Object.freeze<TagResponseDto>({ tag1: Object.freeze<TagResponseDto>({
id: 'tag-1', id: 'tag-1',
createdAt: '2021-01-01T00:00:00.000Z', createdAt: new Date('2021-01-01T00:00:00.000Z'),
updatedAt: '2021-01-01T00:00:00.000Z', updatedAt: new Date('2021-01-01T00:00:00.000Z'),
name: 'Tag1', name: 'Tag1',
value: 'Tag1', value: 'Tag1',
}), }),
color1: Object.freeze<TagResponseDto>({ color1: Object.freeze<TagResponseDto>({
id: 'tag-1', id: 'tag-1',
createdAt: '2021-01-01T00:00:00.000Z', createdAt: new Date('2021-01-01T00:00:00.000Z'),
updatedAt: '2021-01-01T00:00:00.000Z', updatedAt: new Date('2021-01-01T00:00:00.000Z'),
color: '#000000', color: '#000000',
name: 'Tag1', name: 'Tag1',
value: 'Tag1', value: 'Tag1',