diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 11e825a7cd387..c18318adedf5f 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -473,7 +473,7 @@ describe('/asset', () => { expect(body).toMatchObject({ id: user1Assets[0].id, exifInfo: expect.objectContaining({ - dateTimeOriginal: '2023-11-20T01:11:00+00:00', + dateTimeOriginal: new Date('2023-11-19T18:11:00.000-07:00').toISOString(), timeZone: 'UTC-7', }), }); @@ -544,7 +544,7 @@ describe('/asset', () => { await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); const assetInfo = await utils.getAssetInfo(user1.accessToken, id); - expect(assetInfo.exifInfo?.dateTimeOriginal).toBe('2024-07-11T10:32:52+00:00'); + expect(assetInfo.exifInfo?.dateTimeOriginal).toBe(new Date('2024-07-11T10:32:52Z').toISOString()); const { status, body } = await request(app) .put(`/assets/${id}`) @@ -554,7 +554,7 @@ describe('/asset', () => { expect(body).toMatchObject({ id, exifInfo: expect.objectContaining({ - dateTimeOriginal: '2023-11-20T01:11:00+00:00', + dateTimeOriginal: new Date('2023-11-19T18:11:00.000-07:00').toISOString(), }), }); expect(status).toEqual(200); @@ -839,7 +839,7 @@ describe('/asset', () => { expect(result.body).toMatchObject({ id: user1Assets[0].id, exifInfo: expect.objectContaining({ - dateTimeOriginal: '2023-11-19T01:10:00+00:00', + dateTimeOriginal: new Date('2023-11-19T01:10:00.000Z').toISOString(), }), }); }); @@ -868,7 +868,7 @@ describe('/asset', () => { type: AssetTypeEnum.Image, originalFileName: 'el_torcal_rocks.jpg', exifInfo: { - dateTimeOriginal: '2012-08-05T11:39:59+00:00', + dateTimeOriginal: new Date('2012-08-05T11:39:59.000Z').toISOString(), exifImageWidth: 512, exifImageHeight: 341, focalLength: 75, @@ -902,7 +902,7 @@ describe('/asset', () => { originalFileName: 'IMG_2682.heic', fileCreatedAt: '2019-03-21T16:04:22.348Z', exifInfo: { - dateTimeOriginal: '2019-03-21T16:04:22.348+00:00', + dateTimeOriginal: new Date('2019-03-21T16:04:22.348Z').toISOString(), exifImageWidth: 4032, exifImageHeight: 3024, latitude: 41.2203, @@ -945,7 +945,7 @@ describe('/asset', () => { focalLength: 18, iso: 100, fileSizeInByte: 9_057_784, - dateTimeOriginal: '2010-07-20T17:27:12+00:00', + dateTimeOriginal: new Date('2010-07-20T17:27:12.000Z').toISOString(), orientation: '1', }, }, @@ -964,7 +964,7 @@ describe('/asset', () => { focalLength: 85, iso: 200, fileSizeInByte: 15_856_335, - dateTimeOriginal: '2016-09-22T21:10:29.06+00:00', + dateTimeOriginal: new Date('2016-09-22T21:10:29.060Z').toISOString(), orientation: '1', timeZone: 'UTC-4', }, @@ -986,7 +986,7 @@ describe('/asset', () => { focalLength: 35, iso: 400, fileSizeInByte: 19_587_072, - dateTimeOriginal: '2018-05-10T08:42:37.842+00:00', + dateTimeOriginal: new Date('2018-05-10T08:42:37.842Z').toISOString(), orientation: '1', }, }, @@ -1008,7 +1008,7 @@ describe('/asset', () => { iso: 100, lensModel: 'Sony E PZ 18-105mm F4 G OSS', fileSizeInByte: 25_001_984, - dateTimeOriginal: '2016-09-27T10:51:44+00:00', + dateTimeOriginal: new Date('2016-09-27T10:51:44.000Z').toISOString(), orientation: '1', }, }, @@ -1030,7 +1030,7 @@ describe('/asset', () => { iso: 100, lensModel: 'Zeiss Batis 25mm F2', fileSizeInByte: 49_512_448, - dateTimeOriginal: '2016-01-08T14:08:01+00:00', + dateTimeOriginal: new Date('2016-01-08T14:08:01.000Z').toISOString(), orientation: '1', }, }, @@ -1052,7 +1052,7 @@ describe('/asset', () => { iso: 80, lensModel: null, fileSizeInByte: 11_113_617, - dateTimeOriginal: '2015-12-27T09:55:40+00:00', + dateTimeOriginal: new Date('2015-12-27T09:55:40.000Z').toISOString(), latitude: null, longitude: null, orientation: '1', @@ -1076,7 +1076,7 @@ describe('/asset', () => { iso: 160, lensModel: null, fileSizeInByte: 13_551_312, - dateTimeOriginal: '2024-10-12T21:01:01+00:00', + dateTimeOriginal: new Date('2024-10-12T21:01:01.000Z').toISOString(), latitude: null, longitude: null, orientation: '6', @@ -1090,7 +1090,7 @@ describe('/asset', () => { originalFileName: 'Ricoh_GR3-450.DNG', fileCreatedAt: '2024-06-08T13:48:39.000Z', exifInfo: { - dateTimeOriginal: '2024-06-08T13:48:39+00:00', + dateTimeOriginal: new Date('2024-06-08T13:48:39.000Z').toISOString(), exifImageHeight: 4064, exifImageWidth: 6112, exposureTime: '1/400', diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index 793c508a36c4c..6751b21e84d06 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -287,7 +287,8 @@ describe('/admin/users', () => { it('should delete user', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({}); expect(status).toBe(200); expect(body).toMatchObject({ diff --git a/e2e/src/ui/generators/timeline/model-objects.ts b/e2e/src/ui/generators/timeline/model-objects.ts index e300de1161174..5683a72a2384e 100644 --- a/e2e/src/ui/generators/timeline/model-objects.ts +++ b/e2e/src/ui/generators/timeline/model-objects.ts @@ -77,7 +77,7 @@ export function generateAsset( latitude: hasGPS ? faker.location.latitude() : null, longitude: hasGPS ? faker.location.longitude() : null, visibility: AssetVisibility.Timeline, - stack: null, + stack: undefined, fileSizeInByte: faker.number.int({ min: 510, max: 5_000_000 }), checksum: faker.string.alphanumeric({ length: 5 }), }; diff --git a/e2e/src/ui/generators/timeline/timeline-config.ts b/e2e/src/ui/generators/timeline/timeline-config.ts index 992480eef996a..576197a6cb3f7 100644 --- a/e2e/src/ui/generators/timeline/timeline-config.ts +++ b/e2e/src/ui/generators/timeline/timeline-config.ts @@ -52,7 +52,7 @@ export type MockTimelineAsset = { latitude: number | null; longitude: number | null; visibility: AssetVisibility; - stack: null; + stack: undefined; checksum: string; fileSizeInByte: number; }; diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index a3f935c4926b0..45b41b93321fa 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -2,6 +2,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/search_result.model.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; +import 'package:immich_mobile/infrastructure/utils/enum.converter.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart' as api show AssetVisibility; @@ -62,7 +63,7 @@ extension on AssetResponseDto { createdAt: fileCreatedAt, updatedAt: updatedAt, ownerId: ownerId, - visibility: switch (visibility) { + visibility: switch (visibility.toAssetVisibility()) { api.AssetVisibility.timeline => AssetVisibility.timeline, api.AssetVisibility.hidden => AssetVisibility.hidden, api.AssetVisibility.archive => AssetVisibility.archive, @@ -81,13 +82,3 @@ extension on AssetResponseDto { ); } } - -extension on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception('Unknown AssetType value: $this'), - }; -} diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index 2ca0d50dccc8b..a3642d5b16fa8 100644 --- a/mobile/lib/entities/album.entity.dart +++ b/mobile/lib/entities/album.entity.dart @@ -159,7 +159,7 @@ class Album { a.remoteAssetCount = dto.assetCount; a.owner.value = await db.users.getById(dto.ownerId); if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; + a.sortOrder = dto.order!.value == 'asc' ? SortOrder.asc : SortOrder.desc; } if (dto.albumThumbnailAssetId != null) { diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index 0d549457a143c..96028a32ec55c 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; +import 'package:immich_mobile/infrastructure/utils/enum.converter.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/hash.dart'; @@ -25,7 +26,7 @@ class Asset { fileModifiedAt = remote.fileModifiedAt, updatedAt = remote.updatedAt, durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), + type = AssetType.values[remote.type.toAssetType().index], fileName = remote.originalFileName, height = remote.exifInfo?.exifImageHeight?.toInt(), width = remote.exifInfo?.exifImageWidth?.toInt(), @@ -42,7 +43,7 @@ class Asset { stackCount = remote.stack?.assetCount ?? 0, stackId = remote.stack?.id, thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); + visibility = getVisibility(remote.visibility.toAssetVisibility()); Asset({ this.id = Isar.autoIncrement, diff --git a/mobile/lib/infrastructure/utils/enum.converter.dart b/mobile/lib/infrastructure/utils/enum.converter.dart new file mode 100644 index 0000000000000..fe9a5681498e2 --- /dev/null +++ b/mobile/lib/infrastructure/utils/enum.converter.dart @@ -0,0 +1,51 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' show AssetType; +import 'package:openapi/api.dart'; + +// Asset type converter + +AssetType _toAssetTypeFromApiValue(String v) => switch (v) { + 'IMAGE' => AssetType.image, + 'VIDEO' => AssetType.video, + 'AUDIO' => AssetType.audio, + 'OTHER' => AssetType.other, + _ => AssetType.other, +}; + +// Avatar color converter + +AvatarColor _toAvatarColorFromApiValue(String v) => + AvatarColor.values.firstWhere((c) => c.value == v, orElse: () => AvatarColor.primary); + +// Optional: if you still want to canonicalize via the shared model +UserAvatarColor _toUserAvatarColorFromApiValue(String v) => UserAvatarColor.fromJson(v) ?? UserAvatarColor.primary; + +extension UserResponseDtoAvatarColorEnumX on UserResponseDtoAvatarColorEnum { + AvatarColor toAvatarColor() => _toAvatarColorFromApiValue(value); + UserAvatarColor toUserAvatarColor() => _toUserAvatarColorFromApiValue(value); +} + +extension UserAdminResponseDtoAvatarColorEnumX on UserAdminResponseDtoAvatarColorEnum { + AvatarColor toAvatarColor() => _toAvatarColorFromApiValue(value); + UserAvatarColor toUserAvatarColor() => _toUserAvatarColorFromApiValue(value); +} + +extension PartnerResponseDtoAvatarColorEnumX on PartnerResponseDtoAvatarColorEnum { + AvatarColor toAvatarColor() => _toAvatarColorFromApiValue(value); + UserAvatarColor toUserAvatarColor() => _toUserAvatarColorFromApiValue(value); +} + +extension UserUpdateMeDtoAvatarColorEnumX on UserUpdateMeDtoAvatarColorEnum { + AvatarColor toAvatarColor() => _toAvatarColorFromApiValue(value); + UserAvatarColor toUserAvatarColor() => _toUserAvatarColorFromApiValue(value); +} + +extension AssetResponseDtoTypeEnumX on AssetResponseDtoTypeEnum { + AssetType toAssetType() => _toAssetTypeFromApiValue(value); +} + +AssetVisibility _toApiAssetVisibilityFromValue(String v) => AssetVisibility.fromJson(v) ?? AssetVisibility.timeline; + +extension AssetResponseDtoVisibilityEnumX on AssetResponseDtoVisibilityEnum { + AssetVisibility toAssetVisibility() => _toApiAssetVisibilityFromValue(value); +} diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart index 826649b24718a..2e357ca1925d3 100644 --- a/mobile/lib/infrastructure/utils/user.converter.dart +++ b/mobile/lib/infrastructure/utils/user.converter.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/utils/enum.converter.dart'; import 'package:openapi/api.dart'; // TODO: Move to repository once all classes are refactored @@ -47,18 +48,3 @@ abstract final class UserConverter { hasProfileImage: dto.profileImagePath.isNotEmpty, ); } - -extension on UserAvatarColor { - AvatarColor toAvatarColor() => switch (this) { - UserAvatarColor.red => AvatarColor.red, - UserAvatarColor.green => AvatarColor.green, - UserAvatarColor.blue => AvatarColor.blue, - UserAvatarColor.purple => AvatarColor.purple, - UserAvatarColor.orange => AvatarColor.orange, - UserAvatarColor.pink => AvatarColor.pink, - UserAvatarColor.amber => AvatarColor.amber, - UserAvatarColor.yellow => AvatarColor.yellow, - UserAvatarColor.gray => AvatarColor.gray, - UserAvatarColor.primary || _ => AvatarColor.primary, - }; -} diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index 57a1f441eb0ea..1d5f3c21ba051 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -66,11 +66,9 @@ class SharedLink { expiresAt = dto.expiresAt, key = dto.key, showMetadata = dto.showMetadata, - type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual, - title = dto.type == SharedLinkType.ALBUM - ? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE" - : "INDIVIDUAL SHARE", - thumbAssetId = dto.type == SharedLinkType.ALBUM + type = dto.type.value == 'ALBUM' ? SharedLinkSource.album : SharedLinkSource.individual, + title = dto.type.value == 'ALBUM' ? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE" : "INDIVIDUAL SHARE", + thumbAssetId = dto.type.value == 'ALBUM' ? dto.album?.albumThumbnailAssetId : dto.assets.isNotEmpty ? dto.assets[0].id diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 525f0906babf6..51bea4416296b 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -139,7 +139,7 @@ class AlbumApiRepository extends ApiRepository { description: dto.description, endDate: dto.endDate, activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, + sortOrder: dto.order?.value == 'asc' ? SortOrder.asc : SortOrder.desc, ); album.remoteAssetCount = dto.assetCount; album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); @@ -162,7 +162,7 @@ class AlbumApiRepository extends ApiRepository { updatedAt: dto.updatedAt, thumbnailAssetId: dto.albumThumbnailAssetId, isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, + order: dto.order?.value == 'asc' ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, assetCount: dto.assetCount, ownerName: dto.owner.name, isShared: dto.albumUsers.length > 2, diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index 557050323a05f..1ac157d0df139 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -109,7 +109,7 @@ extension on AlbumResponseDto { updatedAt: updatedAt, thumbnailAssetId: albumThumbnailAssetId, isActivityEnabled: isActivityEnabled, - order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, + order: order?.value == 'asc' ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, assetCount: assetCount, ownerName: owner.name, isShared: albumUsers.length > 2, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index afeeb694e1edb..f692be5ea941d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -561,7 +561,6 @@ Class | Method | HTTP request | Description - [SharedLinksUpdate](doc//SharedLinksUpdate.md) - [SignUpDto](doc//SignUpDto.md) - [SmartSearchDto](doc//SmartSearchDto.md) - - [SourceType](doc//SourceType.md) - [StackCreateDto](doc//StackCreateDto.md) - [StackResponseDto](doc//StackResponseDto.md) - [StackUpdateDto](doc//StackUpdateDto.md) @@ -664,7 +663,6 @@ Class | Method | HTTP request | Description - [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md) - [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md) - [UserResponseDto](doc//UserResponseDto.md) - - [UserStatus](doc//UserStatus.md) - [UserUpdateMeDto](doc//UserUpdateMeDto.md) - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md) - [ValidateLibraryDto](doc//ValidateLibraryDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 0d6a98c001398..cb52ada5f72d6 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -300,7 +300,6 @@ part 'model/shared_links_response.dart'; part 'model/shared_links_update.dart'; part 'model/sign_up_dto.dart'; part 'model/smart_search_dto.dart'; -part 'model/source_type.dart'; part 'model/stack_create_dto.dart'; part 'model/stack_response_dto.dart'; part 'model/stack_update_dto.dart'; @@ -403,7 +402,6 @@ part 'model/user_metadata_key.dart'; part 'model/user_preferences_response_dto.dart'; part 'model/user_preferences_update_dto.dart'; part 'model/user_response_dto.dart'; -part 'model/user_status.dart'; part 'model/user_update_me_dto.dart'; part 'model/validate_access_token_response_dto.dart'; part 'model/validate_library_dto.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index 697598ac97f61..183d5f6f7aa89 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -81,6 +81,7 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): + /// Activity ID Future deleteActivityWithHttpInfo(String id,) async { // ignore: prefer_const_declarations final apiPath = r'/activities/{id}' @@ -114,6 +115,7 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): + /// Activity ID Future deleteActivity(String id,) async { final response = await deleteActivityWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 5aabf5cd4b91c..68f7384469d65 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -646,8 +646,6 @@ class ApiClient { return SignUpDto.fromJson(value); case 'SmartSearchDto': return SmartSearchDto.fromJson(value); - case 'SourceType': - return SourceTypeTypeTransformer().decode(value); case 'StackCreateDto': return StackCreateDto.fromJson(value); case 'StackResponseDto': @@ -852,8 +850,6 @@ class ApiClient { return UserPreferencesUpdateDto.fromJson(value); case 'UserResponseDto': return UserResponseDto.fromJson(value); - case 'UserStatus': - return UserStatusTypeTransformer().decode(value); case 'UserUpdateMeDto': return UserUpdateMeDto.fromJson(value); case 'ValidateAccessTokenResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 830325a5b6a1e..b46ff2ec1d850 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -157,9 +157,6 @@ String parameterToString(dynamic value) { if (value is SharedLinkType) { return SharedLinkTypeTypeTransformer().encode(value).toString(); } - if (value is SourceType) { - return SourceTypeTypeTransformer().encode(value).toString(); - } if (value is StorageFolder) { return StorageFolderTypeTransformer().encode(value).toString(); } @@ -184,9 +181,6 @@ String parameterToString(dynamic value) { if (value is UserMetadataKey) { return UserMetadataKeyTypeTransformer().encode(value).toString(); } - if (value is UserStatus) { - return UserStatusTypeTransformer().encode(value).toString(); - } if (value is VideoCodec) { return VideoCodecTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index fb4b6d084e33d..bc220e64ced41 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -40,7 +40,6 @@ class ActivityCreateDto { /// String? comment; - /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index dadb45d8ac655..1b0e279ab7b55 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -33,7 +33,6 @@ class ActivityResponseDto { /// Activity ID String id; - /// Activity type ReactionType type; UserResponseDto user; @@ -72,7 +71,9 @@ class ActivityResponseDto { } else { // json[r'comment'] = 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'type'] = this.type; json[r'user'] = this.user; @@ -90,7 +91,7 @@ class ActivityResponseDto { return ActivityResponseDto( assetId: mapValueOfType(json, r'assetId'), comment: mapValueOfType(json, r'comment'), - 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(json, r'id')!, type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 15ad2a170e331..d9ac019ee226c 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -18,9 +18,15 @@ class ActivityStatisticsResponseDto { }); /// Number of comments + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int comments; /// Number of likes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int likes; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e686fbdc72c..c7a4d18680d63 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -43,6 +43,9 @@ class AlbumResponseDto { List albumUsers; /// Number of assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; List assets; @@ -83,13 +86,7 @@ class AlbumResponseDto { DateTime? lastModifiedAssetTimestamp; /// Asset sort order - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - AssetOrder? order; + AlbumResponseDtoOrderEnum? order; UserResponseDto owner; @@ -171,10 +168,14 @@ class AlbumResponseDto { json[r'assetCount'] = this.assetCount; json[r'assets'] = this.assets; 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; 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 { // json[r'endDate'] = null; } @@ -182,7 +183,9 @@ class AlbumResponseDto { json[r'id'] = this.id; json[r'isActivityEnabled'] = this.isActivityEnabled; 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 { // json[r'lastModifiedAssetTimestamp'] = null; } @@ -195,11 +198,15 @@ class AlbumResponseDto { json[r'ownerId'] = this.ownerId; json[r'shared'] = this.shared; 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 { // 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; } @@ -218,19 +225,19 @@ class AlbumResponseDto { assetCount: mapValueOfType(json, r'assetCount')!, assets: AssetResponseDto.listFromJson(json[r'assets']), 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(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(json, r'hasSharedLink')!, id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, - lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), - order: AssetOrder.fromJson(json[r'order']), + 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: AlbumResponseDtoOrderEnum.fromJson(json[r'order']), owner: UserResponseDto.fromJson(json[r'owner'])!, ownerId: mapValueOfType(json, r'ownerId')!, shared: mapValueOfType(json, r'shared')!, - startDate: mapDateTime(json, r'startDate', r''), - updatedAt: mapDateTime(json, r'updatedAt', 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'/^(?:(?:\\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; @@ -295,3 +302,77 @@ class AlbumResponseDto { }; } +/// Asset sort order +class AlbumResponseDtoOrderEnum { + /// Instantiate a new enum with the provided [value]. + const AlbumResponseDtoOrderEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const asc = AlbumResponseDtoOrderEnum._(r'asc'); + static const desc = AlbumResponseDtoOrderEnum._(r'desc'); + + /// List of all possible values in this [enum][AlbumResponseDtoOrderEnum]. + static const values = [ + asc, + desc, + ]; + + static AlbumResponseDtoOrderEnum? fromJson(dynamic value) => AlbumResponseDtoOrderEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AlbumResponseDtoOrderEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AlbumResponseDtoOrderEnum] to String, +/// and [decode] dynamic data back to [AlbumResponseDtoOrderEnum]. +class AlbumResponseDtoOrderEnumTypeTransformer { + factory AlbumResponseDtoOrderEnumTypeTransformer() => _instance ??= const AlbumResponseDtoOrderEnumTypeTransformer._(); + + const AlbumResponseDtoOrderEnumTypeTransformer._(); + + String encode(AlbumResponseDtoOrderEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AlbumResponseDtoOrderEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AlbumResponseDtoOrderEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'asc': return AlbumResponseDtoOrderEnum.asc; + case r'desc': return AlbumResponseDtoOrderEnum.desc; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AlbumResponseDtoOrderEnumTypeTransformer] instance. + static AlbumResponseDtoOrderEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8d0c01cfb8a43..0b0645cf5b13c 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -18,7 +18,7 @@ class AlbumUserResponseDto { }); /// Album user role - AlbumUserRole role; + AlbumUserResponseDtoRoleEnum role; UserResponseDto user; @@ -52,7 +52,7 @@ class AlbumUserResponseDto { final json = value.cast(); return AlbumUserResponseDto( - role: AlbumUserRole.fromJson(json[r'role'])!, + role: AlbumUserResponseDtoRoleEnum.fromJson(json[r'role'])!, user: UserResponseDto.fromJson(json[r'user'])!, ); } @@ -106,3 +106,77 @@ class AlbumUserResponseDto { }; } +/// Album user role +class AlbumUserResponseDtoRoleEnum { + /// Instantiate a new enum with the provided [value]. + const AlbumUserResponseDtoRoleEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const editor = AlbumUserResponseDtoRoleEnum._(r'editor'); + static const viewer = AlbumUserResponseDtoRoleEnum._(r'viewer'); + + /// List of all possible values in this [enum][AlbumUserResponseDtoRoleEnum]. + static const values = [ + editor, + viewer, + ]; + + static AlbumUserResponseDtoRoleEnum? fromJson(dynamic value) => AlbumUserResponseDtoRoleEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AlbumUserResponseDtoRoleEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AlbumUserResponseDtoRoleEnum] to String, +/// and [decode] dynamic data back to [AlbumUserResponseDtoRoleEnum]. +class AlbumUserResponseDtoRoleEnumTypeTransformer { + factory AlbumUserResponseDtoRoleEnumTypeTransformer() => _instance ??= const AlbumUserResponseDtoRoleEnumTypeTransformer._(); + + const AlbumUserResponseDtoRoleEnumTypeTransformer._(); + + String encode(AlbumUserResponseDtoRoleEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AlbumUserResponseDtoRoleEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AlbumUserResponseDtoRoleEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'editor': return AlbumUserResponseDtoRoleEnum.editor; + case r'viewer': return AlbumUserResponseDtoRoleEnum.viewer; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AlbumUserResponseDtoRoleEnumTypeTransformer] instance. + static AlbumUserResponseDtoRoleEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart index 7351840b11c60..348bae370b5fa 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart @@ -24,7 +24,6 @@ class AssetDeltaSyncResponseDto { /// Whether full sync is needed bool needsFullSync; - /// Upserted assets List upserted; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 61d972a0c493f..023288dd41d08 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -25,37 +25,48 @@ class AssetFaceResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Person associated with face - PersonResponseDto? person; + PersonResponseDto person; /// Face detection source type - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - SourceType? sourceType; + AssetFaceResponseDtoSourceTypeEnum? sourceType; @override bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto && @@ -79,7 +90,7 @@ class AssetFaceResponseDto { (id.hashCode) + (imageHeight.hashCode) + (imageWidth.hashCode) + - (person == null ? 0 : person!.hashCode) + + (person.hashCode) + (sourceType == null ? 0 : sourceType!.hashCode); @override @@ -94,11 +105,7 @@ class AssetFaceResponseDto { json[r'id'] = this.id; json[r'imageHeight'] = this.imageHeight; json[r'imageWidth'] = this.imageWidth; - if (this.person != null) { json[r'person'] = this.person; - } else { - // json[r'person'] = null; - } if (this.sourceType != null) { json[r'sourceType'] = this.sourceType; } else { @@ -123,8 +130,8 @@ class AssetFaceResponseDto { id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, - person: PersonResponseDto.fromJson(json[r'person']), - sourceType: SourceType.fromJson(json[r'sourceType']), + person: PersonResponseDto.fromJson(json[r'person'])!, + sourceType: AssetFaceResponseDtoSourceTypeEnum.fromJson(json[r'sourceType']), ); } return null; @@ -183,3 +190,80 @@ class AssetFaceResponseDto { }; } +/// Face detection source type +class AssetFaceResponseDtoSourceTypeEnum { + /// Instantiate a new enum with the provided [value]. + const AssetFaceResponseDtoSourceTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const machineLearning = AssetFaceResponseDtoSourceTypeEnum._(r'machine-learning'); + static const exif = AssetFaceResponseDtoSourceTypeEnum._(r'exif'); + static const manual = AssetFaceResponseDtoSourceTypeEnum._(r'manual'); + + /// List of all possible values in this [enum][AssetFaceResponseDtoSourceTypeEnum]. + static const values = [ + machineLearning, + exif, + manual, + ]; + + static AssetFaceResponseDtoSourceTypeEnum? fromJson(dynamic value) => AssetFaceResponseDtoSourceTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceResponseDtoSourceTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetFaceResponseDtoSourceTypeEnum] to String, +/// and [decode] dynamic data back to [AssetFaceResponseDtoSourceTypeEnum]. +class AssetFaceResponseDtoSourceTypeEnumTypeTransformer { + factory AssetFaceResponseDtoSourceTypeEnumTypeTransformer() => _instance ??= const AssetFaceResponseDtoSourceTypeEnumTypeTransformer._(); + + const AssetFaceResponseDtoSourceTypeEnumTypeTransformer._(); + + String encode(AssetFaceResponseDtoSourceTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetFaceResponseDtoSourceTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetFaceResponseDtoSourceTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'machine-learning': return AssetFaceResponseDtoSourceTypeEnum.machineLearning; + case r'exif': return AssetFaceResponseDtoSourceTypeEnum.exif; + case r'manual': return AssetFaceResponseDtoSourceTypeEnum.manual; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetFaceResponseDtoSourceTypeEnumTypeTransformer] instance. + static AssetFaceResponseDtoSourceTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 1ae5cef07e9d4..a8abd49a144e4 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -24,34 +24,46 @@ class AssetFaceWithoutPersonResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; /// Face detection source type - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - SourceType? sourceType; + AssetFaceWithoutPersonResponseDtoSourceTypeEnum? sourceType; @override bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto && @@ -112,7 +124,7 @@ class AssetFaceWithoutPersonResponseDto { id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, - sourceType: SourceType.fromJson(json[r'sourceType']), + sourceType: AssetFaceWithoutPersonResponseDtoSourceTypeEnum.fromJson(json[r'sourceType']), ); } return null; @@ -170,3 +182,80 @@ class AssetFaceWithoutPersonResponseDto { }; } +/// Face detection source type +class AssetFaceWithoutPersonResponseDtoSourceTypeEnum { + /// Instantiate a new enum with the provided [value]. + const AssetFaceWithoutPersonResponseDtoSourceTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const machineLearning = AssetFaceWithoutPersonResponseDtoSourceTypeEnum._(r'machine-learning'); + static const exif = AssetFaceWithoutPersonResponseDtoSourceTypeEnum._(r'exif'); + static const manual = AssetFaceWithoutPersonResponseDtoSourceTypeEnum._(r'manual'); + + /// List of all possible values in this [enum][AssetFaceWithoutPersonResponseDtoSourceTypeEnum]. + static const values = [ + machineLearning, + exif, + manual, + ]; + + static AssetFaceWithoutPersonResponseDtoSourceTypeEnum? fromJson(dynamic value) => AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceWithoutPersonResponseDtoSourceTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetFaceWithoutPersonResponseDtoSourceTypeEnum] to String, +/// and [decode] dynamic data back to [AssetFaceWithoutPersonResponseDtoSourceTypeEnum]. +class AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer { + factory AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer() => _instance ??= const AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer._(); + + const AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer._(); + + String encode(AssetFaceWithoutPersonResponseDtoSourceTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetFaceWithoutPersonResponseDtoSourceTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetFaceWithoutPersonResponseDtoSourceTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'machine-learning': return AssetFaceWithoutPersonResponseDtoSourceTypeEnum.machineLearning; + case r'exif': return AssetFaceWithoutPersonResponseDtoSourceTypeEnum.exif; + case r'manual': return AssetFaceWithoutPersonResponseDtoSourceTypeEnum.manual; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer] instance. + static AssetFaceWithoutPersonResponseDtoSourceTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 5422ccf55f9de..5c9acee371db6 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -86,6 +86,8 @@ class AssetResponseDto { bool hasMetadata; /// Asset height + /// + /// Minimum value: 0 num? height; /// Asset ID @@ -152,6 +154,12 @@ class AssetResponseDto { /// bool? resized; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// AssetStackResponseDto? stack; List tags; @@ -160,7 +168,7 @@ class AssetResponseDto { String? thumbhash; /// Asset type - AssetTypeEnum type; + AssetResponseDtoTypeEnum type; List unassignedFaces; @@ -168,9 +176,11 @@ class AssetResponseDto { DateTime updatedAt; /// Asset visibility - AssetVisibility visibility; + AssetResponseDtoVisibilityEnum visibility; /// Asset width + /// + /// Minimum value: 0 num? width; @override @@ -256,7 +266,9 @@ class AssetResponseDto { Map toJson() { final json = {}; 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(); json[r'deviceAssetId'] = this.deviceAssetId; json[r'deviceId'] = this.deviceId; if (this.duplicateId != null) { @@ -270,8 +282,12 @@ class AssetResponseDto { } else { // json[r'exifInfo'] = null; } - json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); - json[r'fileModifiedAt'] = this.fileModifiedAt.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))$/') + ? 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; if (this.height != null) { json[r'height'] = this.height; @@ -294,7 +310,9 @@ class AssetResponseDto { } else { // 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; if (this.originalMimeType != null) { json[r'originalMimeType'] = this.originalMimeType; @@ -327,7 +345,9 @@ class AssetResponseDto { } json[r'type'] = this.type; 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; if (this.width != null) { json[r'width'] = this.width; @@ -347,14 +367,14 @@ class AssetResponseDto { return AssetResponseDto( checksum: mapValueOfType(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))$/')!, deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, deviceId: mapValueOfType(json, r'deviceId')!, duplicateId: mapValueOfType(json, r'duplicateId'), duration: mapValueOfType(json, r'duration')!, exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', 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'/^(?:(?:\\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(json, r'hasMetadata')!, height: json[r'height'] == null ? null @@ -367,7 +387,7 @@ class AssetResponseDto { isTrashed: mapValueOfType(json, r'isTrashed')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(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(json, r'originalFileName')!, originalMimeType: mapValueOfType(json, r'originalMimeType'), originalPath: mapValueOfType(json, r'originalPath')!, @@ -378,10 +398,10 @@ class AssetResponseDto { stack: AssetStackResponseDto.fromJson(json[r'stack']), tags: TagResponseDto.listFromJson(json[r'tags']), thumbhash: mapValueOfType(json, r'thumbhash'), - type: AssetTypeEnum.fromJson(json[r'type'])!, + type: AssetResponseDtoTypeEnum.fromJson(json[r'type'])!, unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - visibility: AssetVisibility.fromJson(json[r'visibility'])!, + 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: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!, width: json[r'width'] == null ? null : num.parse('${json[r'width']}'), @@ -459,3 +479,163 @@ class AssetResponseDto { }; } +/// Asset type +class AssetResponseDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const AssetResponseDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const IMAGE = AssetResponseDtoTypeEnum._(r'IMAGE'); + static const VIDEO = AssetResponseDtoTypeEnum._(r'VIDEO'); + static const AUDIO = AssetResponseDtoTypeEnum._(r'AUDIO'); + static const OTHER = AssetResponseDtoTypeEnum._(r'OTHER'); + + /// List of all possible values in this [enum][AssetResponseDtoTypeEnum]. + static const values = [ + IMAGE, + VIDEO, + AUDIO, + OTHER, + ]; + + static AssetResponseDtoTypeEnum? fromJson(dynamic value) => AssetResponseDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetResponseDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetResponseDtoTypeEnum] to String, +/// and [decode] dynamic data back to [AssetResponseDtoTypeEnum]. +class AssetResponseDtoTypeEnumTypeTransformer { + factory AssetResponseDtoTypeEnumTypeTransformer() => _instance ??= const AssetResponseDtoTypeEnumTypeTransformer._(); + + const AssetResponseDtoTypeEnumTypeTransformer._(); + + String encode(AssetResponseDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetResponseDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'IMAGE': return AssetResponseDtoTypeEnum.IMAGE; + case r'VIDEO': return AssetResponseDtoTypeEnum.VIDEO; + case r'AUDIO': return AssetResponseDtoTypeEnum.AUDIO; + case r'OTHER': return AssetResponseDtoTypeEnum.OTHER; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetResponseDtoTypeEnumTypeTransformer] instance. + static AssetResponseDtoTypeEnumTypeTransformer? _instance; +} + + +/// Asset visibility +class AssetResponseDtoVisibilityEnum { + /// Instantiate a new enum with the provided [value]. + const AssetResponseDtoVisibilityEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const archive = AssetResponseDtoVisibilityEnum._(r'archive'); + static const timeline = AssetResponseDtoVisibilityEnum._(r'timeline'); + static const hidden = AssetResponseDtoVisibilityEnum._(r'hidden'); + static const locked = AssetResponseDtoVisibilityEnum._(r'locked'); + + /// List of all possible values in this [enum][AssetResponseDtoVisibilityEnum]. + static const values = [ + archive, + timeline, + hidden, + locked, + ]; + + static AssetResponseDtoVisibilityEnum? fromJson(dynamic value) => AssetResponseDtoVisibilityEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetResponseDtoVisibilityEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetResponseDtoVisibilityEnum] to String, +/// and [decode] dynamic data back to [AssetResponseDtoVisibilityEnum]. +class AssetResponseDtoVisibilityEnumTypeTransformer { + factory AssetResponseDtoVisibilityEnumTypeTransformer() => _instance ??= const AssetResponseDtoVisibilityEnumTypeTransformer._(); + + const AssetResponseDtoVisibilityEnumTypeTransformer._(); + + String encode(AssetResponseDtoVisibilityEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetResponseDtoVisibilityEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetResponseDtoVisibilityEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'archive': return AssetResponseDtoVisibilityEnum.archive; + case r'timeline': return AssetResponseDtoVisibilityEnum.timeline; + case r'hidden': return AssetResponseDtoVisibilityEnum.hidden; + case r'locked': return AssetResponseDtoVisibilityEnum.locked; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetResponseDtoVisibilityEnumTypeTransformer] instance. + static AssetResponseDtoVisibilityEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 229e7aa710023..96fd66a392ac2 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -19,6 +19,9 @@ class AssetStackResponseDto { }); /// Number of assets in stack + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// Stack ID diff --git a/mobile/openapi/lib/model/asset_type_enum.dart b/mobile/openapi/lib/model/asset_type_enum.dart index b6e0351198d32..fe79f78e4139c 100644 --- a/mobile/openapi/lib/model/asset_type_enum.dart +++ b/mobile/openapi/lib/model/asset_type_enum.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Asset type +/// Asset type filter class AssetTypeEnum { /// Instantiate a new enum with the provided [value]. const AssetTypeEnum._(this.value); diff --git a/mobile/openapi/lib/model/asset_visibility.dart b/mobile/openapi/lib/model/asset_visibility.dart index 6290dffb2eea0..498bf17c38242 100644 --- a/mobile/openapi/lib/model/asset_visibility.dart +++ b/mobile/openapi/lib/model/asset_visibility.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Asset visibility + class AssetVisibility { /// Instantiate a new enum with the provided [value]. const AssetVisibility._(this.value); diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index 1bef8f29d829b..af5b2cbf68c32 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -18,6 +18,9 @@ class ContributorCountResponseDto { }); /// Number of assets contributed + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// User ID diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart index 6c85dc8013713..8f980031d5eff 100644 --- a/mobile/openapi/lib/model/duplicate_response_dto.dart +++ b/mobile/openapi/lib/model/duplicate_response_dto.dart @@ -17,7 +17,6 @@ class DuplicateResponseDto { required this.duplicateId, }); - /// Duplicate assets List assets; /// Duplicate group ID diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 6bb58a8ab98e2..1544718fd0140 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -50,10 +50,16 @@ class ExifResponseDto { String? description; /// Image height in pixels - num? exifImageHeight; + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 + int? exifImageHeight; /// Image width in pixels - num? exifImageWidth; + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 + int? exifImageWidth; /// Exposure time String? exposureTime; @@ -62,6 +68,9 @@ class ExifResponseDto { num? fNumber; /// File size in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length in mm @@ -170,7 +179,9 @@ class ExifResponseDto { // json[r'country'] = 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 { // json[r'dateTimeOriginal'] = null; } @@ -240,7 +251,9 @@ class ExifResponseDto { // json[r'model'] = 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 { // json[r'modifyDate'] = null; } @@ -283,14 +296,10 @@ class ExifResponseDto { return ExifResponseDto( city: mapValueOfType(json, r'city'), country: mapValueOfType(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(json, r'description'), - exifImageHeight: json[r'exifImageHeight'] == null - ? null - : num.parse('${json[r'exifImageHeight']}'), - exifImageWidth: json[r'exifImageWidth'] == null - ? null - : num.parse('${json[r'exifImageWidth']}'), + exifImageHeight: mapValueOfType(json, r'exifImageHeight'), + exifImageWidth: mapValueOfType(json, r'exifImageWidth'), exposureTime: mapValueOfType(json, r'exposureTime'), fNumber: json[r'fNumber'] == null ? null @@ -311,7 +320,7 @@ class ExifResponseDto { : num.parse('${json[r'longitude']}'), make: mapValueOfType(json, r'make'), model: mapValueOfType(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(json, r'orientation'), projectionType: mapValueOfType(json, r'projectionType'), rating: json[r'rating'] == null diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 7fd938b31a0e5..bd23d374ed841 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -48,7 +48,7 @@ class MemoryCreateDto { DateTime? seenAt; /// Memory type - MemoryType type; + MemoryCreateDtoTypeEnum type; @override bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && @@ -81,9 +81,13 @@ class MemoryCreateDto { } else { // json[r'isSaved'] = null; } - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } @@ -105,9 +109,9 @@ class MemoryCreateDto { : const [], data: OnThisDayDto.fromJson(json[r'data'])!, isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r'')!, - seenAt: mapDateTime(json, r'seenAt', r''), - type: MemoryType.fromJson(json[r'type'])!, + memoryAt: mapDateTime(json, r'memoryAt', 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))$/')!, + seenAt: mapDateTime(json, r'seenAt', 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))$/'), + type: MemoryCreateDtoTypeEnum.fromJson(json[r'type'])!, ); } return null; @@ -161,3 +165,74 @@ class MemoryCreateDto { }; } +/// Memory type +class MemoryCreateDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const MemoryCreateDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryCreateDtoTypeEnum._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryCreateDtoTypeEnum]. + static const values = [ + onThisDay, + ]; + + static MemoryCreateDtoTypeEnum? fromJson(dynamic value) => MemoryCreateDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryCreateDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryCreateDtoTypeEnum] to String, +/// and [decode] dynamic data back to [MemoryCreateDtoTypeEnum]. +class MemoryCreateDtoTypeEnumTypeTransformer { + factory MemoryCreateDtoTypeEnumTypeTransformer() => _instance ??= const MemoryCreateDtoTypeEnumTypeTransformer._(); + + const MemoryCreateDtoTypeEnumTypeTransformer._(); + + String encode(MemoryCreateDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryCreateDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryCreateDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryCreateDtoTypeEnum.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryCreateDtoTypeEnumTypeTransformer] instance. + static MemoryCreateDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 1835095cf7270..cd7e749058b82 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -84,7 +84,7 @@ class MemoryResponseDto { DateTime? showAt; /// Memory type - MemoryType type; + MemoryResponseDtoTypeEnum type; /// Last update date DateTime updatedAt; @@ -128,34 +128,48 @@ class MemoryResponseDto { Map toJson() { final json = {}; json[r'assets'] = this.assets; - 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'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _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.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _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.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _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.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _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.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _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.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - 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; } @@ -169,18 +183,18 @@ class MemoryResponseDto { return MemoryResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), - 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))$/')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', 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))$/'), + hideAt: mapDateTime(json, r'hideAt', 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(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', 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))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), - type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + seenAt: mapDateTime(json, r'seenAt', 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))$/'), + showAt: mapDateTime(json, r'showAt', 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))$/'), + type: MemoryResponseDtoTypeEnum.fromJson(json[r'type'])!, + 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; @@ -240,3 +254,74 @@ class MemoryResponseDto { }; } +/// Memory type +class MemoryResponseDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const MemoryResponseDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryResponseDtoTypeEnum._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryResponseDtoTypeEnum]. + static const values = [ + onThisDay, + ]; + + static MemoryResponseDtoTypeEnum? fromJson(dynamic value) => MemoryResponseDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponseDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryResponseDtoTypeEnum] to String, +/// and [decode] dynamic data back to [MemoryResponseDtoTypeEnum]. +class MemoryResponseDtoTypeEnumTypeTransformer { + factory MemoryResponseDtoTypeEnumTypeTransformer() => _instance ??= const MemoryResponseDtoTypeEnumTypeTransformer._(); + + const MemoryResponseDtoTypeEnumTypeTransformer._(); + + String encode(MemoryResponseDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryResponseDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryResponseDtoTypeEnum.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryResponseDtoTypeEnumTypeTransformer] instance. + static MemoryResponseDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index 93ec956f5801c..b1dac50fff482 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -19,7 +19,8 @@ class OnThisDayDto { /// Year for on this day memory /// /// Minimum value: 1 - num year; + /// Maximum value: 9999 + int year; @override bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && @@ -48,7 +49,7 @@ class OnThisDayDto { final json = value.cast(); return OnThisDayDto( - year: num.parse('${json[r'year']}'), + year: mapValueOfType(json, r'year')!, ); } return null; diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5789938d187b2..d95c0cb868b53 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,8 +22,7 @@ class PartnerResponseDto { required this.profileImagePath, }); - /// Avatar color - UserAvatarColor avatarColor; + PartnerResponseDtoAvatarColorEnum avatarColor; /// User email String email; @@ -84,7 +83,9 @@ class PartnerResponseDto { // json[r'inTimeline'] = null; } 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; return json; } @@ -98,12 +99,12 @@ class PartnerResponseDto { final json = value.cast(); return PartnerResponseDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, + avatarColor: PartnerResponseDtoAvatarColorEnum.fromJson(json[r'avatarColor'])!, email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, inTimeline: mapValueOfType(json, r'inTimeline'), name: mapValueOfType(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(json, r'profileImagePath')!, ); } @@ -161,3 +162,101 @@ class PartnerResponseDto { }; } + +class PartnerResponseDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const PartnerResponseDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = PartnerResponseDtoAvatarColorEnum._(r'primary'); + static const pink = PartnerResponseDtoAvatarColorEnum._(r'pink'); + static const red = PartnerResponseDtoAvatarColorEnum._(r'red'); + static const yellow = PartnerResponseDtoAvatarColorEnum._(r'yellow'); + static const blue = PartnerResponseDtoAvatarColorEnum._(r'blue'); + static const green = PartnerResponseDtoAvatarColorEnum._(r'green'); + static const purple = PartnerResponseDtoAvatarColorEnum._(r'purple'); + static const orange = PartnerResponseDtoAvatarColorEnum._(r'orange'); + static const gray = PartnerResponseDtoAvatarColorEnum._(r'gray'); + static const amber = PartnerResponseDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][PartnerResponseDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static PartnerResponseDtoAvatarColorEnum? fromJson(dynamic value) => PartnerResponseDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PartnerResponseDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [PartnerResponseDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [PartnerResponseDtoAvatarColorEnum]. +class PartnerResponseDtoAvatarColorEnumTypeTransformer { + factory PartnerResponseDtoAvatarColorEnumTypeTransformer() => _instance ??= const PartnerResponseDtoAvatarColorEnumTypeTransformer._(); + + const PartnerResponseDtoAvatarColorEnumTypeTransformer._(); + + String encode(PartnerResponseDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a PartnerResponseDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + PartnerResponseDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return PartnerResponseDtoAvatarColorEnum.primary; + case r'pink': return PartnerResponseDtoAvatarColorEnum.pink; + case r'red': return PartnerResponseDtoAvatarColorEnum.red; + case r'yellow': return PartnerResponseDtoAvatarColorEnum.yellow; + case r'blue': return PartnerResponseDtoAvatarColorEnum.blue; + case r'green': return PartnerResponseDtoAvatarColorEnum.green; + case r'purple': return PartnerResponseDtoAvatarColorEnum.purple; + case r'orange': return PartnerResponseDtoAvatarColorEnum.orange; + case r'gray': return PartnerResponseDtoAvatarColorEnum.gray; + case r'amber': return PartnerResponseDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PartnerResponseDtoAvatarColorEnumTypeTransformer] instance. + static PartnerResponseDtoAvatarColorEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index f345657e739ea..87edc6b4a71eb 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -29,12 +29,17 @@ class PeopleResponseDto { bool? hasNextPage; /// Number of hidden people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int hidden; - /// List of people List people; /// Total number of people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index 455dfb98d6fde..d6ae66faf166a 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -94,7 +94,9 @@ class PersonResponseDto { Map toJson() { final json = {}; 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 { // json[r'birthDate'] = null; } @@ -113,7 +115,9 @@ class PersonResponseDto { json[r'name'] = this.name; json[r'thumbnailPath'] = this.thumbnailPath; 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 { // json[r'updatedAt'] = null; } @@ -129,14 +133,14 @@ class PersonResponseDto { final json = value.cast(); 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(json, r'color'), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite'), isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, thumbnailPath: mapValueOfType(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; diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index f31c04b69ff90..f08853e8a487b 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -36,7 +36,6 @@ class PersonWithFacesResponseDto { /// String? color; - /// Face detections List faces; /// Person ID @@ -100,7 +99,9 @@ class PersonWithFacesResponseDto { Map toJson() { final json = {}; 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 { // json[r'birthDate'] = null; } @@ -120,7 +121,9 @@ class PersonWithFacesResponseDto { json[r'name'] = this.name; json[r'thumbnailPath'] = this.thumbnailPath; 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 { // json[r'updatedAt'] = null; } @@ -136,7 +139,7 @@ class PersonWithFacesResponseDto { final json = value.cast(); 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(json, r'color'), faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']), id: mapValueOfType(json, r'id')!, @@ -144,7 +147,7 @@ class PersonWithFacesResponseDto { isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, thumbnailPath: mapValueOfType(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; diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart index 29568b9d1103d..6060f4c2b78fe 100644 --- a/mobile/openapi/lib/model/reaction_level.dart +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction level class ReactionLevel { /// Instantiate a new enum with the provided [value]. const ReactionLevel._(this.value); diff --git a/mobile/openapi/lib/model/reaction_type.dart b/mobile/openapi/lib/model/reaction_type.dart index 4c788138fbcb6..c4daccad716d8 100644 --- a/mobile/openapi/lib/model/reaction_type.dart +++ b/mobile/openapi/lib/model/reaction_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction type class ReactionType { /// Instantiate a new enum with the provided [value]. const ReactionType._(this.value); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 8841251e4adcb..c21113ee6d25e 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -20,6 +20,9 @@ class SearchAlbumResponseDto { }); /// Number of albums in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -27,6 +30,9 @@ class SearchAlbumResponseDto { List items; /// Total number of matching albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index acb81f28e28d8..f4ffade26b797 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -21,6 +21,9 @@ class SearchAssetResponseDto { }); /// Number of assets in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -31,6 +34,9 @@ class SearchAssetResponseDto { String? nextPage; /// Total number of matching assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index 8318fbfb3bcc0..62adfaa74ac4e 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -18,6 +18,9 @@ class SearchFacetCountResponseDto { }); /// Number of assets with this facet value + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; /// Facet value diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 43b5ac5c81219..51124ef1cfb16 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,7 +17,6 @@ class SearchFacetResponseDto { required this.fieldName, }); - /// Facet counts List counts; /// Facet field name diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d9aec48c39391..746dc10b94f9b 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -74,7 +74,7 @@ class SharedLinkResponseDto { String? token; /// Shared link type - SharedLinkType type; + SharedLinkResponseDtoTypeEnum type; /// Owner user ID String userId; @@ -129,14 +129,18 @@ class SharedLinkResponseDto { json[r'allowDownload'] = this.allowDownload; json[r'allowUpload'] = this.allowUpload; json[r'assets'] = this.assets; - 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.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _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.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -176,16 +180,16 @@ class SharedLinkResponseDto { allowDownload: mapValueOfType(json, r'allowDownload')!, allowUpload: mapValueOfType(json, r'allowUpload')!, assets: AssetResponseDto.listFromJson(json[r'assets']), - 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(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', 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(json, r'id')!, key: mapValueOfType(json, r'key')!, password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata')!, slug: mapValueOfType(json, r'slug'), token: mapValueOfType(json, r'token'), - type: SharedLinkType.fromJson(json[r'type'])!, + type: SharedLinkResponseDtoTypeEnum.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); } @@ -250,3 +254,77 @@ class SharedLinkResponseDto { }; } +/// Shared link type +class SharedLinkResponseDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const SharedLinkResponseDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const ALBUM = SharedLinkResponseDtoTypeEnum._(r'ALBUM'); + static const INDIVIDUAL = SharedLinkResponseDtoTypeEnum._(r'INDIVIDUAL'); + + /// List of all possible values in this [enum][SharedLinkResponseDtoTypeEnum]. + static const values = [ + ALBUM, + INDIVIDUAL, + ]; + + static SharedLinkResponseDtoTypeEnum? fromJson(dynamic value) => SharedLinkResponseDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SharedLinkResponseDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [SharedLinkResponseDtoTypeEnum] to String, +/// and [decode] dynamic data back to [SharedLinkResponseDtoTypeEnum]. +class SharedLinkResponseDtoTypeEnumTypeTransformer { + factory SharedLinkResponseDtoTypeEnumTypeTransformer() => _instance ??= const SharedLinkResponseDtoTypeEnumTypeTransformer._(); + + const SharedLinkResponseDtoTypeEnumTypeTransformer._(); + + String encode(SharedLinkResponseDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a SharedLinkResponseDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + SharedLinkResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'ALBUM': return SharedLinkResponseDtoTypeEnum.ALBUM; + case r'INDIVIDUAL': return SharedLinkResponseDtoTypeEnum.INDIVIDUAL; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [SharedLinkResponseDtoTypeEnumTypeTransformer] instance. + static SharedLinkResponseDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/source_type.dart b/mobile/openapi/lib/model/source_type.dart deleted file mode 100644 index ed164172a370b..0000000000000 --- a/mobile/openapi/lib/model/source_type.dart +++ /dev/null @@ -1,88 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -/// Face detection source type -class SourceType { - /// Instantiate a new enum with the provided [value]. - const SourceType._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const machineLearning = SourceType._(r'machine-learning'); - static const exif = SourceType._(r'exif'); - static const manual = SourceType._(r'manual'); - - /// List of all possible values in this [enum][SourceType]. - static const values = [ - machineLearning, - exif, - manual, - ]; - - static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = SourceType.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [SourceType] to String, -/// and [decode] dynamic data back to [SourceType]. -class SourceTypeTypeTransformer { - factory SourceTypeTypeTransformer() => _instance ??= const SourceTypeTypeTransformer._(); - - const SourceTypeTypeTransformer._(); - - String encode(SourceType data) => data.value; - - /// Decodes a [dynamic value][data] to a SourceType. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - SourceType? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'machine-learning': return SourceType.machineLearning; - case r'exif': return SourceType.exif; - case r'manual': return SourceType.manual; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [SourceTypeTypeTransformer] instance. - static SourceTypeTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 638dfb52557cc..326f83a03df37 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,7 +18,6 @@ class StackResponseDto { required this.primaryAssetId, }); - /// Stack assets List assets; /// Stack ID diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index 9a71912153c9b..6440690527b4e 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -86,7 +86,9 @@ class TagResponseDto { } else { // 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'name'] = this.name; if (this.parentId != null) { @@ -94,7 +96,9 @@ class TagResponseDto { } else { // 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; return json; } @@ -109,11 +113,11 @@ class TagResponseDto { return TagResponseDto( color: mapValueOfType(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(json, r'id')!, name: mapValueOfType(json, r'name')!, parentId: mapValueOfType(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(json, r'value')!, ); } diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 485b2e00e50d6..a50bc34416022 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -25,8 +25,7 @@ class UserAdminCreateDto { this.storageLabel, }); - /// Avatar color - UserAvatarColor? avatarColor; + UserAdminCreateDtoAvatarColorEnum? avatarColor; /// User email String email; @@ -61,6 +60,7 @@ class UserAdminCreateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login @@ -157,7 +157,7 @@ class UserAdminCreateDto { final json = value.cast(); return UserAdminCreateDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), + avatarColor: UserAdminCreateDtoAvatarColorEnum.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email')!, isAdmin: mapValueOfType(json, r'isAdmin'), name: mapValueOfType(json, r'name')!, @@ -220,3 +220,101 @@ class UserAdminCreateDto { }; } + +class UserAdminCreateDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const UserAdminCreateDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserAdminCreateDtoAvatarColorEnum._(r'primary'); + static const pink = UserAdminCreateDtoAvatarColorEnum._(r'pink'); + static const red = UserAdminCreateDtoAvatarColorEnum._(r'red'); + static const yellow = UserAdminCreateDtoAvatarColorEnum._(r'yellow'); + static const blue = UserAdminCreateDtoAvatarColorEnum._(r'blue'); + static const green = UserAdminCreateDtoAvatarColorEnum._(r'green'); + static const purple = UserAdminCreateDtoAvatarColorEnum._(r'purple'); + static const orange = UserAdminCreateDtoAvatarColorEnum._(r'orange'); + static const gray = UserAdminCreateDtoAvatarColorEnum._(r'gray'); + static const amber = UserAdminCreateDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][UserAdminCreateDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserAdminCreateDtoAvatarColorEnum? fromJson(dynamic value) => UserAdminCreateDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserAdminCreateDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserAdminCreateDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [UserAdminCreateDtoAvatarColorEnum]. +class UserAdminCreateDtoAvatarColorEnumTypeTransformer { + factory UserAdminCreateDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserAdminCreateDtoAvatarColorEnumTypeTransformer._(); + + const UserAdminCreateDtoAvatarColorEnumTypeTransformer._(); + + String encode(UserAdminCreateDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserAdminCreateDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserAdminCreateDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserAdminCreateDtoAvatarColorEnum.primary; + case r'pink': return UserAdminCreateDtoAvatarColorEnum.pink; + case r'red': return UserAdminCreateDtoAvatarColorEnum.red; + case r'yellow': return UserAdminCreateDtoAvatarColorEnum.yellow; + case r'blue': return UserAdminCreateDtoAvatarColorEnum.blue; + case r'green': return UserAdminCreateDtoAvatarColorEnum.green; + case r'purple': return UserAdminCreateDtoAvatarColorEnum.purple; + case r'orange': return UserAdminCreateDtoAvatarColorEnum.orange; + case r'gray': return UserAdminCreateDtoAvatarColorEnum.gray; + case r'amber': return UserAdminCreateDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserAdminCreateDtoAvatarColorEnumTypeTransformer] instance. + static UserAdminCreateDtoAvatarColorEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 706f65cf35a32..81f002b071e8d 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,8 +32,7 @@ class UserAdminResponseDto { required this.updatedAt, }); - /// Avatar color - UserAvatarColor avatarColor; + UserAdminResponseDtoAvatarColorEnum avatarColor; /// Creation date DateTime createdAt; @@ -50,8 +49,7 @@ class UserAdminResponseDto { /// Is admin user bool isAdmin; - /// User license - UserLicense? license; + UserLicense license; /// User name String name; @@ -66,16 +64,22 @@ class UserAdminResponseDto { String profileImagePath; /// Storage quota in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Storage usage in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaUsageInBytes; /// Require password change on next login bool shouldChangePassword; /// User status - UserStatus status; + UserAdminResponseDtoStatusEnum status; /// Storage label String? storageLabel; @@ -112,7 +116,7 @@ class UserAdminResponseDto { (email.hashCode) + (id.hashCode) + (isAdmin.hashCode) + - (license == null ? 0 : license!.hashCode) + + (license.hashCode) + (name.hashCode) + (oauthId.hashCode) + (profileChangedAt.hashCode) + @@ -130,23 +134,25 @@ class UserAdminResponseDto { Map toJson() { final json = {}; json[r'avatarColor'] = this.avatarColor; - 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.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _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.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } json[r'email'] = this.email; json[r'id'] = this.id; json[r'isAdmin'] = this.isAdmin; - if (this.license != null) { json[r'license'] = this.license; - } else { - // json[r'license'] = null; - } json[r'name'] = this.name; 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; if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; @@ -165,7 +171,9 @@ class UserAdminResponseDto { } else { // json[r'storageLabel'] = 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; } @@ -178,23 +186,23 @@ class UserAdminResponseDto { final json = value.cast(); return UserAdminResponseDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, - createdAt: mapDateTime(json, r'createdAt', r'')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + avatarColor: UserAdminResponseDtoAvatarColorEnum.fromJson(json[r'avatarColor'])!, + 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))$/')!, + deletedAt: mapDateTime(json, r'deletedAt', 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))$/'), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, - license: UserLicense.fromJson(json[r'license']), + license: UserLicense.fromJson(json[r'license'])!, name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(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(json, r'profileImagePath')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, - status: UserStatus.fromJson(json[r'status'])!, + status: UserAdminResponseDtoStatusEnum.fromJson(json[r'status'])!, storageLabel: mapValueOfType(json, r'storageLabel'), - 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; @@ -262,3 +270,178 @@ class UserAdminResponseDto { }; } + +class UserAdminResponseDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const UserAdminResponseDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserAdminResponseDtoAvatarColorEnum._(r'primary'); + static const pink = UserAdminResponseDtoAvatarColorEnum._(r'pink'); + static const red = UserAdminResponseDtoAvatarColorEnum._(r'red'); + static const yellow = UserAdminResponseDtoAvatarColorEnum._(r'yellow'); + static const blue = UserAdminResponseDtoAvatarColorEnum._(r'blue'); + static const green = UserAdminResponseDtoAvatarColorEnum._(r'green'); + static const purple = UserAdminResponseDtoAvatarColorEnum._(r'purple'); + static const orange = UserAdminResponseDtoAvatarColorEnum._(r'orange'); + static const gray = UserAdminResponseDtoAvatarColorEnum._(r'gray'); + static const amber = UserAdminResponseDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][UserAdminResponseDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserAdminResponseDtoAvatarColorEnum? fromJson(dynamic value) => UserAdminResponseDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserAdminResponseDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserAdminResponseDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [UserAdminResponseDtoAvatarColorEnum]. +class UserAdminResponseDtoAvatarColorEnumTypeTransformer { + factory UserAdminResponseDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserAdminResponseDtoAvatarColorEnumTypeTransformer._(); + + const UserAdminResponseDtoAvatarColorEnumTypeTransformer._(); + + String encode(UserAdminResponseDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserAdminResponseDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserAdminResponseDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserAdminResponseDtoAvatarColorEnum.primary; + case r'pink': return UserAdminResponseDtoAvatarColorEnum.pink; + case r'red': return UserAdminResponseDtoAvatarColorEnum.red; + case r'yellow': return UserAdminResponseDtoAvatarColorEnum.yellow; + case r'blue': return UserAdminResponseDtoAvatarColorEnum.blue; + case r'green': return UserAdminResponseDtoAvatarColorEnum.green; + case r'purple': return UserAdminResponseDtoAvatarColorEnum.purple; + case r'orange': return UserAdminResponseDtoAvatarColorEnum.orange; + case r'gray': return UserAdminResponseDtoAvatarColorEnum.gray; + case r'amber': return UserAdminResponseDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserAdminResponseDtoAvatarColorEnumTypeTransformer] instance. + static UserAdminResponseDtoAvatarColorEnumTypeTransformer? _instance; +} + + +/// User status +class UserAdminResponseDtoStatusEnum { + /// Instantiate a new enum with the provided [value]. + const UserAdminResponseDtoStatusEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const active = UserAdminResponseDtoStatusEnum._(r'active'); + static const removing = UserAdminResponseDtoStatusEnum._(r'removing'); + static const deleted = UserAdminResponseDtoStatusEnum._(r'deleted'); + + /// List of all possible values in this [enum][UserAdminResponseDtoStatusEnum]. + static const values = [ + active, + removing, + deleted, + ]; + + static UserAdminResponseDtoStatusEnum? fromJson(dynamic value) => UserAdminResponseDtoStatusEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserAdminResponseDtoStatusEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserAdminResponseDtoStatusEnum] to String, +/// and [decode] dynamic data back to [UserAdminResponseDtoStatusEnum]. +class UserAdminResponseDtoStatusEnumTypeTransformer { + factory UserAdminResponseDtoStatusEnumTypeTransformer() => _instance ??= const UserAdminResponseDtoStatusEnumTypeTransformer._(); + + const UserAdminResponseDtoStatusEnumTypeTransformer._(); + + String encode(UserAdminResponseDtoStatusEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserAdminResponseDtoStatusEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserAdminResponseDtoStatusEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'active': return UserAdminResponseDtoStatusEnum.active; + case r'removing': return UserAdminResponseDtoStatusEnum.removing; + case r'deleted': return UserAdminResponseDtoStatusEnum.deleted; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserAdminResponseDtoStatusEnumTypeTransformer] instance. + static UserAdminResponseDtoStatusEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 3cce65745f6af..bf5bbc54a1657 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,8 +24,7 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// Avatar color - UserAvatarColor? avatarColor; + UserAdminUpdateDtoAvatarColorEnum? avatarColor; /// User email /// @@ -69,6 +68,7 @@ class UserAdminUpdateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login @@ -170,7 +170,7 @@ class UserAdminUpdateDto { final json = value.cast(); return UserAdminUpdateDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), + avatarColor: UserAdminUpdateDtoAvatarColorEnum.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), isAdmin: mapValueOfType(json, r'isAdmin'), name: mapValueOfType(json, r'name'), @@ -229,3 +229,101 @@ class UserAdminUpdateDto { }; } + +class UserAdminUpdateDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const UserAdminUpdateDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserAdminUpdateDtoAvatarColorEnum._(r'primary'); + static const pink = UserAdminUpdateDtoAvatarColorEnum._(r'pink'); + static const red = UserAdminUpdateDtoAvatarColorEnum._(r'red'); + static const yellow = UserAdminUpdateDtoAvatarColorEnum._(r'yellow'); + static const blue = UserAdminUpdateDtoAvatarColorEnum._(r'blue'); + static const green = UserAdminUpdateDtoAvatarColorEnum._(r'green'); + static const purple = UserAdminUpdateDtoAvatarColorEnum._(r'purple'); + static const orange = UserAdminUpdateDtoAvatarColorEnum._(r'orange'); + static const gray = UserAdminUpdateDtoAvatarColorEnum._(r'gray'); + static const amber = UserAdminUpdateDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][UserAdminUpdateDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserAdminUpdateDtoAvatarColorEnum? fromJson(dynamic value) => UserAdminUpdateDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserAdminUpdateDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserAdminUpdateDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [UserAdminUpdateDtoAvatarColorEnum]. +class UserAdminUpdateDtoAvatarColorEnumTypeTransformer { + factory UserAdminUpdateDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserAdminUpdateDtoAvatarColorEnumTypeTransformer._(); + + const UserAdminUpdateDtoAvatarColorEnumTypeTransformer._(); + + String encode(UserAdminUpdateDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserAdminUpdateDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserAdminUpdateDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserAdminUpdateDtoAvatarColorEnum.primary; + case r'pink': return UserAdminUpdateDtoAvatarColorEnum.pink; + case r'red': return UserAdminUpdateDtoAvatarColorEnum.red; + case r'yellow': return UserAdminUpdateDtoAvatarColorEnum.yellow; + case r'blue': return UserAdminUpdateDtoAvatarColorEnum.blue; + case r'green': return UserAdminUpdateDtoAvatarColorEnum.green; + case r'purple': return UserAdminUpdateDtoAvatarColorEnum.purple; + case r'orange': return UserAdminUpdateDtoAvatarColorEnum.orange; + case r'gray': return UserAdminUpdateDtoAvatarColorEnum.gray; + case r'amber': return UserAdminUpdateDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserAdminUpdateDtoAvatarColorEnumTypeTransformer] instance. + static UserAdminUpdateDtoAvatarColorEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index f02dc73befcfd..8ef46a0bb55be 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -24,7 +24,7 @@ class UserLicense { /// Activation key String activationKey; - /// License key + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override @@ -45,7 +45,9 @@ class UserLicense { Map toJson() { final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); + json[r'activatedAt'] = _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.activatedAt.millisecondsSinceEpoch + : this.activatedAt.toUtc().toIso8601String(); json[r'activationKey'] = this.activationKey; json[r'licenseKey'] = this.licenseKey; return json; @@ -60,7 +62,7 @@ class UserLicense { final json = value.cast(); return UserLicense( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, + activatedAt: mapDateTime(json, r'activatedAt', 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))$/')!, activationKey: mapValueOfType(json, r'activationKey')!, licenseKey: mapValueOfType(json, r'licenseKey')!, ); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index bf0e2cbf09cbc..5b1708088952a 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,8 +21,7 @@ class UserResponseDto { required this.profileImagePath, }); - /// Avatar color - UserAvatarColor avatarColor; + UserResponseDtoAvatarColorEnum avatarColor; /// User email String email; @@ -67,7 +66,9 @@ class UserResponseDto { json[r'email'] = this.email; json[r'id'] = this.id; 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; return json; } @@ -81,11 +82,11 @@ class UserResponseDto { final json = value.cast(); return UserResponseDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, + avatarColor: UserResponseDtoAvatarColorEnum.fromJson(json[r'avatarColor'])!, email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(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(json, r'profileImagePath')!, ); } @@ -143,3 +144,101 @@ class UserResponseDto { }; } + +class UserResponseDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const UserResponseDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserResponseDtoAvatarColorEnum._(r'primary'); + static const pink = UserResponseDtoAvatarColorEnum._(r'pink'); + static const red = UserResponseDtoAvatarColorEnum._(r'red'); + static const yellow = UserResponseDtoAvatarColorEnum._(r'yellow'); + static const blue = UserResponseDtoAvatarColorEnum._(r'blue'); + static const green = UserResponseDtoAvatarColorEnum._(r'green'); + static const purple = UserResponseDtoAvatarColorEnum._(r'purple'); + static const orange = UserResponseDtoAvatarColorEnum._(r'orange'); + static const gray = UserResponseDtoAvatarColorEnum._(r'gray'); + static const amber = UserResponseDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][UserResponseDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserResponseDtoAvatarColorEnum? fromJson(dynamic value) => UserResponseDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserResponseDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserResponseDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [UserResponseDtoAvatarColorEnum]. +class UserResponseDtoAvatarColorEnumTypeTransformer { + factory UserResponseDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserResponseDtoAvatarColorEnumTypeTransformer._(); + + const UserResponseDtoAvatarColorEnumTypeTransformer._(); + + String encode(UserResponseDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserResponseDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserResponseDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserResponseDtoAvatarColorEnum.primary; + case r'pink': return UserResponseDtoAvatarColorEnum.pink; + case r'red': return UserResponseDtoAvatarColorEnum.red; + case r'yellow': return UserResponseDtoAvatarColorEnum.yellow; + case r'blue': return UserResponseDtoAvatarColorEnum.blue; + case r'green': return UserResponseDtoAvatarColorEnum.green; + case r'purple': return UserResponseDtoAvatarColorEnum.purple; + case r'orange': return UserResponseDtoAvatarColorEnum.orange; + case r'gray': return UserResponseDtoAvatarColorEnum.gray; + case r'amber': return UserResponseDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserResponseDtoAvatarColorEnumTypeTransformer] instance. + static UserResponseDtoAvatarColorEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/user_status.dart b/mobile/openapi/lib/model/user_status.dart deleted file mode 100644 index 130bd650f219e..0000000000000 --- a/mobile/openapi/lib/model/user_status.dart +++ /dev/null @@ -1,88 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -/// User status -class UserStatus { - /// Instantiate a new enum with the provided [value]. - const UserStatus._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const active = UserStatus._(r'active'); - static const removing = UserStatus._(r'removing'); - static const deleted = UserStatus._(r'deleted'); - - /// List of all possible values in this [enum][UserStatus]. - static const values = [ - active, - removing, - deleted, - ]; - - static UserStatus? fromJson(dynamic value) => UserStatusTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = UserStatus.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [UserStatus] to String, -/// and [decode] dynamic data back to [UserStatus]. -class UserStatusTypeTransformer { - factory UserStatusTypeTransformer() => _instance ??= const UserStatusTypeTransformer._(); - - const UserStatusTypeTransformer._(); - - String encode(UserStatus data) => data.value; - - /// Decodes a [dynamic value][data] to a UserStatus. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - UserStatus? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'active': return UserStatus.active; - case r'removing': return UserStatus.removing; - case r'deleted': return UserStatus.deleted; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [UserStatusTypeTransformer] instance. - static UserStatusTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 066c435eb304c..7ac48fb12b58d 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,8 +19,7 @@ class UserUpdateMeDto { this.password, }); - /// Avatar color - UserAvatarColor? avatarColor; + UserUpdateMeDtoAvatarColorEnum? avatarColor; /// User email /// @@ -101,7 +100,7 @@ class UserUpdateMeDto { final json = value.cast(); return UserUpdateMeDto( - avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), + avatarColor: UserUpdateMeDtoAvatarColorEnum.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), @@ -155,3 +154,101 @@ class UserUpdateMeDto { }; } + +class UserUpdateMeDtoAvatarColorEnum { + /// Instantiate a new enum with the provided [value]. + const UserUpdateMeDtoAvatarColorEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserUpdateMeDtoAvatarColorEnum._(r'primary'); + static const pink = UserUpdateMeDtoAvatarColorEnum._(r'pink'); + static const red = UserUpdateMeDtoAvatarColorEnum._(r'red'); + static const yellow = UserUpdateMeDtoAvatarColorEnum._(r'yellow'); + static const blue = UserUpdateMeDtoAvatarColorEnum._(r'blue'); + static const green = UserUpdateMeDtoAvatarColorEnum._(r'green'); + static const purple = UserUpdateMeDtoAvatarColorEnum._(r'purple'); + static const orange = UserUpdateMeDtoAvatarColorEnum._(r'orange'); + static const gray = UserUpdateMeDtoAvatarColorEnum._(r'gray'); + static const amber = UserUpdateMeDtoAvatarColorEnum._(r'amber'); + + /// List of all possible values in this [enum][UserUpdateMeDtoAvatarColorEnum]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserUpdateMeDtoAvatarColorEnum? fromJson(dynamic value) => UserUpdateMeDtoAvatarColorEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = UserUpdateMeDtoAvatarColorEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserUpdateMeDtoAvatarColorEnum] to String, +/// and [decode] dynamic data back to [UserUpdateMeDtoAvatarColorEnum]. +class UserUpdateMeDtoAvatarColorEnumTypeTransformer { + factory UserUpdateMeDtoAvatarColorEnumTypeTransformer() => _instance ??= const UserUpdateMeDtoAvatarColorEnumTypeTransformer._(); + + const UserUpdateMeDtoAvatarColorEnumTypeTransformer._(); + + String encode(UserUpdateMeDtoAvatarColorEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a UserUpdateMeDtoAvatarColorEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + UserUpdateMeDtoAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserUpdateMeDtoAvatarColorEnum.primary; + case r'pink': return UserUpdateMeDtoAvatarColorEnum.pink; + case r'red': return UserUpdateMeDtoAvatarColorEnum.red; + case r'yellow': return UserUpdateMeDtoAvatarColorEnum.yellow; + case r'blue': return UserUpdateMeDtoAvatarColorEnum.blue; + case r'green': return UserUpdateMeDtoAvatarColorEnum.green; + case r'purple': return UserUpdateMeDtoAvatarColorEnum.purple; + case r'orange': return UserUpdateMeDtoAvatarColorEnum.orange; + case r'gray': return UserUpdateMeDtoAvatarColorEnum.gray; + case r'amber': return UserUpdateMeDtoAvatarColorEnum.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserUpdateMeDtoAvatarColorEnumTypeTransformer] instance. + static UserUpdateMeDtoAvatarColorEnumTypeTransformer? _instance; +} + + diff --git a/mobile/test/default.isar-lck b/mobile/test/default.isar-lck new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0e57fc4819795..13cdb060953cc 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -13,6 +13,7 @@ "description": "Album ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -23,6 +24,7 @@ "description": "Asset ID (if activity is for an asset)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -51,6 +53,7 @@ "description": "Filter by user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -173,6 +176,7 @@ "description": "Album ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -183,6 +187,7 @@ "description": "Asset ID (if activity is for an asset)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -241,8 +246,10 @@ "name": "id", "required": true, "in": "path", + "description": "Activity ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -936,6 +943,7 @@ "description": "User ID filter", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15417,11 +15425,13 @@ "albumId": { "description": "Album ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "assetId": { "description": "Asset ID (if activity is for an asset)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15429,12 +15439,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type (like or comment)" + "$ref": "#/components/schemas/ReactionType" } }, "required": [ @@ -15447,7 +15452,9 @@ "properties": { "assetId": { "description": "Asset ID (if activity is for an asset)", + "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15458,19 +15465,17 @@ "createdAt": { "description": "Creation date", "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" }, "id": { "description": "Activity ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type" + "$ref": "#/components/schemas/ReactionType" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15489,10 +15494,14 @@ "properties": { "comments": { "description": "Number of comments", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "likes": { "description": "Number of likes", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15549,6 +15558,8 @@ }, "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "assets": { @@ -15566,6 +15577,7 @@ "createdAt": { "description": "Creation date", "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" }, "description": { @@ -15575,6 +15587,7 @@ "endDate": { "description": "End date (latest asset)", "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" }, "hasSharedLink": { @@ -15592,15 +15605,17 @@ "lastModifiedAssetTimestamp": { "description": "Last modified asset timestamp", "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" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } + "description": "Asset sort order", + "enum": [ + "asc", + "desc" ], - "description": "Asset sort order" + "title": "AssetOrder", + "type": "string" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" @@ -15616,11 +15631,13 @@ "startDate": { "description": "Start date (earliest asset)", "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" }, "updatedAt": { "description": "Last update date", "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" } }, @@ -15711,12 +15728,13 @@ "AlbumUserResponseDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } + "description": "Album user role", + "enum": [ + "editor", + "viewer" ], - "description": "Album user role" + "title": "AlbumUserRole", + "type": "string" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -16059,7 +16077,6 @@ "type": "boolean" }, "upserted": { - "description": "Upserted assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -16281,49 +16298,59 @@ "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "person": { - "allOf": [ - { - "$ref": "#/components/schemas/PersonResponseDto" - } - ], - "description": "Person associated with face", + "$ref": "#/components/schemas/PersonResponseDto", "nullable": true }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } + "description": "Face detection source type", + "enum": [ + "machine-learning", + "exif", + "manual" ], - "description": "Face detection source type" + "title": "SourceType", + "type": "string" } }, "required": [ @@ -16373,43 +16400,59 @@ "type": "object" }, "AssetFaceWithoutPersonResponseDto": { + "description": "Asset face without person", "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } + "description": "Face detection source type", + "enum": [ + "machine-learning", + "exif", + "manual" ], - "description": "Face detection source type" + "title": "SourceType", + "type": "string" } }, "required": [ @@ -16935,8 +16978,8 @@ }, "createdAt": { "description": "The UTC timestamp when the asset was originally uploaded to Immich.", - "example": "2024-01-15T20:30:00.000Z", "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" }, "deviceAssetId": { @@ -16961,14 +17004,14 @@ }, "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.", - "example": "2024-01-15T19:30:00.000Z", "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" }, "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.", - "example": "2024-01-16T10:15:00.000Z", "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" }, "hasMetadata": { @@ -16977,6 +17020,7 @@ }, "height": { "description": "Asset height", + "minimum": 0, "nullable": true, "type": "number" }, @@ -16990,18 +17034,7 @@ }, "isEdited": { "description": "Is edited", - "type": "boolean", - "x-immich-history": [ - { - "version": "v2.5.0", - "state": "Added" - }, - { - "version": "v2.5.0", - "state": "Beta" - } - ], - "x-immich-state": "Beta" + "type": "boolean" }, "isFavorite": { "description": "Is favorite", @@ -17016,22 +17049,11 @@ "type": "boolean" }, "libraryId": { - "deprecated": true, "description": "Library ID", "format": "uuid", "nullable": true, - "type": "string", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, "livePhotoVideoId": { "description": "Live photo video ID", @@ -17040,8 +17062,8 @@ }, "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.", - "example": "2024-01-15T14:30:00.000Z", "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" }, "originalFileName": { @@ -17070,27 +17092,11 @@ "type": "array" }, "resized": { - "deprecated": true, "description": "Is resized", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1.113.0", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" + "type": "boolean" }, "stack": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetStackResponseDto" - } - ], + "$ref": "#/components/schemas/AssetStackResponseDto", "nullable": true }, "tags": { @@ -17105,12 +17111,15 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } + "description": "Asset type", + "enum": [ + "IMAGE", + "VIDEO", + "AUDIO", + "OTHER" ], - "description": "Asset type" + "title": "AssetTypeEnum", + "type": "string" }, "unassignedFaces": { "items": { @@ -17120,20 +17129,24 @@ }, "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.", - "example": "2024-01-16T12:45:30.000Z", "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" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } + "description": "Asset visibility", + "enum": [ + "archive", + "timeline", + "hidden", + "locked" ], - "description": "Asset visibility" + "title": "AssetVisibility", + "type": "string" }, "width": { "description": "Asset width", + "minimum": 0, "nullable": true, "type": "number" } @@ -17170,6 +17183,8 @@ "properties": { "assetCount": { "description": "Number of assets in stack", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "id": { @@ -17211,7 +17226,7 @@ "type": "object" }, "AssetTypeEnum": { - "description": "Asset type", + "description": "Asset type filter", "enum": [ "IMAGE", "VIDEO", @@ -17221,7 +17236,6 @@ "type": "string" }, "AssetVisibility": { - "description": "Asset visibility", "enum": [ "archive", "timeline", @@ -17457,6 +17471,8 @@ "properties": { "assetCount": { "description": "Number of assets contributed", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "userId": { @@ -17821,7 +17837,6 @@ "DuplicateResponseDto": { "properties": { "assets": { - "description": "Duplicate assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -17878,138 +17893,124 @@ "type": "object" }, "ExifResponseDto": { + "description": "EXIF response", "properties": { "city": { - "default": null, "description": "City name", "nullable": true, "type": "string" }, "country": { - "default": null, "description": "Country name", "nullable": true, "type": "string" }, "dateTimeOriginal": { - "default": null, "description": "Original date/time", "format": "date-time", "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" }, "description": { - "default": null, "description": "Image description", "nullable": true, "type": "string" }, "exifImageHeight": { - "default": null, "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, - "type": "number" + "type": "integer" }, "exifImageWidth": { - "default": null, "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, - "type": "number" + "type": "integer" }, "exposureTime": { - "default": null, "description": "Exposure time", "nullable": true, "type": "string" }, "fNumber": { - "default": null, "description": "F-number (aperture)", "nullable": true, "type": "number" }, "fileSizeInByte": { - "default": null, "description": "File size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, "focalLength": { - "default": null, "description": "Focal length in mm", "nullable": true, "type": "number" }, "iso": { - "default": null, "description": "ISO sensitivity", "nullable": true, "type": "number" }, "latitude": { - "default": null, "description": "GPS latitude", "nullable": true, "type": "number" }, "lensModel": { - "default": null, "description": "Lens model", "nullable": true, "type": "string" }, "longitude": { - "default": null, "description": "GPS longitude", "nullable": true, "type": "number" }, "make": { - "default": null, "description": "Camera make", "nullable": true, "type": "string" }, "model": { - "default": null, "description": "Camera model", "nullable": true, "type": "string" }, "modifyDate": { - "default": null, "description": "Modification date/time", "format": "date-time", "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" }, "orientation": { - "default": null, "description": "Image orientation", "nullable": true, "type": "string" }, "projectionType": { - "default": null, "description": "Projection type", "nullable": true, "type": "string" }, "rating": { - "default": null, "description": "Rating", "nullable": true, "type": "number" }, "state": { - "default": null, "description": "State/province name", "nullable": true, "type": "string" }, "timeZone": { - "default": null, "description": "Time zone", "nullable": true, "type": "string" @@ -18674,12 +18675,14 @@ "description": "Asset IDs to associate with memory", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" }, "data": { - "$ref": "#/components/schemas/OnThisDayDto" + "$ref": "#/components/schemas/OnThisDayDto", + "description": "Memory type-specific data" }, "isSaved": { "description": "Is memory saved", @@ -18688,20 +18691,22 @@ "memoryAt": { "description": "Memory date", "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" }, "seenAt": { "description": "Date when memory was seen", "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": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } + "description": "Memory type", + "enum": [ + "on_this_day" ], - "description": "Memory type" + "title": "MemoryType", + "type": "string" } }, "required": [ @@ -18722,6 +18727,7 @@ "createdAt": { "description": "Creation date", "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" }, "data": { @@ -18730,11 +18736,13 @@ "deletedAt": { "description": "Deletion date", "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" }, "hideAt": { "description": "Date when memory should be hidden", "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" }, "id": { @@ -18748,6 +18756,7 @@ "memoryAt": { "description": "Memory date", "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" }, "ownerId": { @@ -18757,24 +18766,27 @@ "seenAt": { "description": "Date when memory was seen", "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" }, "showAt": { "description": "Date when memory should be shown", "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": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } + "description": "Memory type", + "enum": [ + "on_this_day" ], - "description": "Memory type" + "title": "MemoryType", + "type": "string" }, "updatedAt": { "description": "Last update date", "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" } }, @@ -19381,8 +19393,9 @@ "properties": { "year": { "description": "Year for on this day memory", + "maximum": 9999, "minimum": 1, - "type": "number" + "type": "integer" } }, "required": [ @@ -19437,12 +19450,20 @@ "PartnerResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color" + "title": "UserAvatarColor", + "type": "string" }, "email": { "description": "User email", @@ -19450,6 +19471,8 @@ }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "inTimeline": { @@ -19463,6 +19486,7 @@ "profileChangedAt": { "description": "Profile change date", "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" }, "profileImagePath": { @@ -19515,25 +19539,15 @@ "properties": { "hasNextPage": { "description": "Whether there are more pages", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1.110.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "type": "boolean" }, "hidden": { "description": "Number of hidden people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "people": { - "description": "List of people", "items": { "$ref": "#/components/schemas/PersonResponseDto" }, @@ -19541,6 +19555,8 @@ }, "total": { "description": "Total number of people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -19815,22 +19831,12 @@ "description": "Person date of birth", "format": "date", "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" }, "color": { "description": "Person color (hex)", - "type": "string", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "type": "string" }, "id": { "description": "Person ID", @@ -19838,18 +19844,7 @@ }, "isFavorite": { "description": "Is favorite", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "type": "boolean" }, "isHidden": { "description": "Is hidden", @@ -19866,18 +19861,8 @@ "updatedAt": { "description": "Last update date", "format": "date-time", - "type": "string", - "x-immich-history": [ - { - "version": "v1.107.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "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" } }, "required": [ @@ -19940,25 +19925,14 @@ "description": "Person date of birth", "format": "date", "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" }, "color": { "description": "Person color (hex)", - "type": "string", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "type": "string" }, "faces": { - "description": "Face detections", "items": { "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" }, @@ -19970,18 +19944,7 @@ }, "isFavorite": { "description": "Is favorite", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "type": "boolean" }, "isHidden": { "description": "Is hidden", @@ -19998,18 +19961,8 @@ "updatedAt": { "description": "Last update date", "format": "date-time", - "type": "string", - "x-immich-history": [ - { - "version": "v1.107.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "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" } }, "required": [ @@ -20825,6 +20778,7 @@ "type": "object" }, "ReactionLevel": { + "description": "Reaction level", "enum": [ "album", "asset" @@ -20832,6 +20786,7 @@ "type": "string" }, "ReactionType": { + "description": "Reaction type", "enum": [ "comment", "like" @@ -20873,6 +20828,8 @@ "properties": { "count": { "description": "Number of albums in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -20889,6 +20846,8 @@ }, "total": { "description": "Total number of matching albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -20904,6 +20863,8 @@ "properties": { "count": { "description": "Number of assets in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -20925,6 +20886,8 @@ }, "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -20976,6 +20939,8 @@ "properties": { "count": { "description": "Number of assets with this facet value", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "value": { @@ -20992,7 +20957,6 @@ "SearchFacetResponseDto": { "properties": { "counts": { - "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, @@ -21829,6 +21793,7 @@ "createdAt": { "description": "Creation date", "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" }, "description": { @@ -21840,6 +21805,7 @@ "description": "Expiration date", "format": "date-time", "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" }, "id": { @@ -21865,33 +21831,18 @@ "type": "string" }, "token": { - "deprecated": true, "description": "Access token", "nullable": true, - "type": "string", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" + "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } + "description": "Shared link type", + "enum": [ + "ALBUM", + "INDIVIDUAL" ], - "description": "Shared link type" + "title": "SharedLinkType", + "type": "string" }, "userId": { "description": "Owner user ID", @@ -22168,15 +22119,6 @@ }, "type": "object" }, - "SourceType": { - "description": "Face detection source type", - "enum": [ - "machine-learning", - "exif", - "manual" - ], - "type": "string" - }, "StackCreateDto": { "properties": { "assetIds": { @@ -22197,7 +22139,6 @@ "StackResponseDto": { "properties": { "assets": { - "description": "Stack assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -24665,6 +24606,7 @@ "createdAt": { "description": "Creation date", "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" }, "id": { @@ -24682,6 +24624,7 @@ "updatedAt": { "description": "Last update date", "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" }, "value": { @@ -25194,17 +25137,26 @@ "UserAdminCreateDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color", - "nullable": true + "nullable": true, + "title": "UserAvatarColor", + "type": "string" }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25225,13 +25177,13 @@ }, "pinCode": { "description": "PIN code", - "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25265,22 +25217,32 @@ "UserAdminResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color" + "title": "UserAvatarColor", + "type": "string" }, "createdAt": { "description": "Creation date", "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" }, "deletedAt": { "description": "Deletion date", "format": "date-time", "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" }, "email": { @@ -25289,6 +25251,8 @@ }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isAdmin": { @@ -25296,12 +25260,7 @@ "type": "boolean" }, "license": { - "allOf": [ - { - "$ref": "#/components/schemas/UserLicense" - } - ], - "description": "User license", + "$ref": "#/components/schemas/UserLicense", "nullable": true }, "name": { @@ -25315,6 +25274,7 @@ "profileChangedAt": { "description": "Profile change date", "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" }, "profileImagePath": { @@ -25323,13 +25283,15 @@ }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -25338,12 +25300,14 @@ "type": "boolean" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/UserStatus" - } + "description": "User status", + "enum": [ + "active", + "removing", + "deleted" ], - "description": "User status" + "title": "UserStatus", + "type": "string" }, "storageLabel": { "description": "Storage label", @@ -25353,6 +25317,7 @@ "updatedAt": { "description": "Last update date", "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" } }, @@ -25380,17 +25345,26 @@ "UserAdminUpdateDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color", - "nullable": true + "nullable": true, + "title": "UserAvatarColor", + "type": "string" }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25407,13 +25381,13 @@ }, "pinCode": { "description": "PIN code", - "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25451,6 +25425,7 @@ "activatedAt": { "description": "Activation date", "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" }, "activationKey": { @@ -25458,7 +25433,8 @@ "type": "string" }, "licenseKey": { - "description": "License key", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -25573,12 +25549,20 @@ "UserResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color" + "title": "UserAvatarColor", + "type": "string" }, "email": { "description": "User email", @@ -25586,6 +25570,8 @@ }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "name": { @@ -25595,6 +25581,7 @@ "profileChangedAt": { "description": "Profile change date", "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" }, "profileImagePath": { @@ -25612,29 +25599,29 @@ ], "type": "object" }, - "UserStatus": { - "description": "User status", - "enum": [ - "active", - "removing", - "deleted" - ], - "type": "string" - }, "UserUpdateMeDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], - "description": "Avatar color", - "nullable": true + "nullable": true, + "title": "UserAvatarColor", + "type": "string" }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -25642,6 +25629,7 @@ "type": "string" }, "password": { + "deprecated": true, "description": "User password (deprecated, use change password endpoint)", "type": "string" } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index acd8109cd3805..129c3eadda9b8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -15,7 +15,6 @@ export const servers = { server1: "/api" }; export type UserResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -37,7 +36,6 @@ export type ActivityResponseDto = { createdAt: string; /** Activity ID */ id: string; - /** Activity type */ "type": ReactionType; user: UserResponseDto; }; @@ -48,7 +46,6 @@ export type ActivityCreateDto = { assetId?: string; /** Comment text (required if type is comment) */ comment?: string; - /** Activity type (like or comment) */ "type": ReactionType; }; export type ActivityStatisticsResponseDto = { @@ -181,11 +178,10 @@ export type UserLicense = { activatedAt: string; /** Activation key */ activationKey: string; - /** License key */ + /** License key (format: /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/) */ licenseKey: string; }; export type UserAdminResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** Creation date */ createdAt: string; @@ -197,8 +193,7 @@ export type UserAdminResponseDto = { id: string; /** Is admin user */ isAdmin: boolean; - /** User license */ - license: (UserLicense) | null; + license: UserLicense; /** User name */ name: string; /** OAuth ID */ @@ -221,8 +216,7 @@ export type UserAdminResponseDto = { updatedAt: string; }; export type UserAdminCreateDto = { - /** Avatar color */ - avatarColor?: (UserAvatarColor) | null; + avatarColor?: UserAvatarColor | null; /** User email */ email: string; /** Grant admin privileges */ @@ -247,8 +241,7 @@ export type UserAdminDeleteDto = { force?: boolean; }; export type UserAdminUpdateDto = { - /** Avatar color */ - avatarColor?: (UserAvatarColor) | null; + avatarColor?: UserAvatarColor | null; /** User email */ email?: string; /** Grant admin privileges */ @@ -523,7 +516,6 @@ export type PersonWithFacesResponseDto = { birthDate: string | null; /** Person color (hex) */ color?: string; - /** Face detections */ faces: AssetFaceWithoutPersonResponseDto[]; /** Person ID */ id: string; @@ -614,7 +606,7 @@ export type AssetResponseDto = { people?: PersonWithFacesResponseDto[]; /** Is resized */ resized?: boolean; - stack?: (AssetStackResponseDto) | null; + stack?: AssetStackResponseDto; tags?: TagResponseDto[]; /** Thumbhash for thumbnail generation */ thumbhash: string | null; @@ -1161,7 +1153,6 @@ export type DownloadResponseDto = { totalSize: number; }; export type DuplicateResponseDto = { - /** Duplicate assets */ assets: AssetResponseDto[]; /** Duplicate group ID */ duplicateId: string; @@ -1199,8 +1190,7 @@ export type AssetFaceResponseDto = { imageHeight: number; /** Image width in pixels */ imageWidth: number; - /** Person associated with face */ - person: (PersonResponseDto) | null; + person: PersonResponseDto; /** Face detection source type */ sourceType?: SourceType; }; @@ -1405,6 +1395,7 @@ export type MemoryResponseDto = { export type MemoryCreateDto = { /** Asset IDs to associate with memory */ assetIds?: string[]; + /** Memory type-specific data */ data: OnThisDayDto; /** Is memory saved */ isSaved?: boolean; @@ -1462,7 +1453,6 @@ export type OAuthCallbackDto = { url: string; }; export type PartnerResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -1490,7 +1480,6 @@ export type PeopleResponseDto = { hasNextPage?: boolean; /** Number of hidden people */ hidden: number; - /** List of people */ people: PersonResponseDto[]; /** Total number of people */ total: number; @@ -1751,7 +1740,6 @@ export type SearchFacetCountResponseDto = { value: string; }; export type SearchFacetResponseDto = { - /** Facet counts */ counts: SearchFacetCountResponseDto[]; /** Facet field name */ fieldName: string; @@ -2324,7 +2312,6 @@ export type AssetIdsResponseDto = { success: boolean; }; export type StackResponseDto = { - /** Stack assets */ assets: AssetResponseDto[]; /** Stack ID */ id: string; @@ -2364,7 +2351,6 @@ export type AssetDeltaSyncResponseDto = { deleted: string[]; /** Whether full sync is needed */ needsFullSync: boolean; - /** Upserted assets */ upserted: AssetResponseDto[]; }; export type AssetFullSyncDto = { @@ -2798,8 +2784,7 @@ export type TrashResponseDto = { count: number; }; export type UserUpdateMeDto = { - /** Avatar color */ - avatarColor?: (UserAvatarColor) | null; + avatarColor?: UserAvatarColor | null; /** User email */ email?: string; /** User name */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e8f0c84b3266..3732736b19d3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -511,6 +511,9 @@ importers: nestjs-otel: specifier: ^7.0.0 version: 7.0.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + nestjs-zod: + specifier: ^5.1.1 + version: 5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^7.0.0 version: 7.0.13 @@ -580,6 +583,9 @@ importers: validator: specifier: ^13.12.0 version: 13.15.26 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@eslint/js': specifier: ^9.8.0 @@ -9111,6 +9117,17 @@ packages: '@nestjs/common': '>= 11 < 12' '@nestjs/core': '>= 11 < 12' + nestjs-zod@5.1.1: + resolution: {integrity: sha512-pXa9Jrdip7iedKvGxJTvvCFVRCoIcNENPCsHjpCefPH3PcFejRgkZkUcr3TYITRyxnUk7Zy5OsLpirZGLYBfBQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -12141,6 +12158,9 @@ packages: zod@4.2.1: resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -21912,6 +21932,15 @@ snapshots: response-time: 2.3.4 tslib: 2.8.1 + nestjs-zod@5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + optionalDependencies: + '@nestjs/swagger': 11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + next-tick@1.1.0: {} no-case@3.0.4: @@ -25577,6 +25606,8 @@ snapshots: zod@4.2.1: {} + zod@4.3.6: {} + zwitch@1.0.5: {} zwitch@2.0.4: {} diff --git a/server/package.json b/server/package.json index fa10f8bd1ab90..eea85731fc4d8 100644 --- a/server/package.json +++ b/server/package.json @@ -91,6 +91,7 @@ "nestjs-cls": "^5.0.0", "nestjs-kysely": "3.1.2", "nestjs-otel": "^7.0.0", + "nestjs-zod": "^5.1.1", "nodemailer": "^7.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", @@ -113,7 +114,8 @@ "transformation-matrix": "^3.1.0", "ua-parser-js": "^2.0.0", "uuid": "^11.1.0", - "validator": "^13.12.0" + "validator": "^13.12.0", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 49b779ca1833c..c0ab93e3ea100 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,10 +1,11 @@ import { BullModule } from '@nestjs/bullmq'; -import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; +import { Inject, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; +import { ZodSerializerInterceptor } from 'nestjs-zod'; import { commandsAndQuestions } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; @@ -18,6 +19,7 @@ import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; +import { HybridValidationPipe } from 'src/middleware/hybrid-validation.pipe'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { AppRepository } from 'src/repositories/app.repository'; @@ -41,7 +43,8 @@ const common = [...repositories, ...services, GlobalExceptionFilter]; const commonMiddleware = [ { provide: APP_FILTER, useClass: GlobalExceptionFilter }, - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_PIPE, useClass: HybridValidationPipe }, + { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, ]; diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts index bf2038048fc1e..78a1b1a52a80c 100644 --- a/server/src/controllers/activity.controller.spec.ts +++ b/server/src/controllers/activity.controller.spec.ts @@ -27,13 +27,15 @@ describe(ActivityController.name, () => { it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + factory.responses.badRequest(expect.arrayContaining(['Invalid input: expected string, received undefined'])), + ); }); it('should reject an invalid albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['Invalid UUID']))); }); it('should reject an invalid assetId', async () => { @@ -41,7 +43,7 @@ describe(ActivityController.name, () => { .get('/activities') .query({ albumId: factory.uuid(), assetId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['Invalid UUID']))); }); }); @@ -54,7 +56,11 @@ describe(ActivityController.name, () => { it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining(['Invalid UUID', expect.stringContaining('Invalid option: expected one of')]), + ), + ); }); it('should require a comment when type is comment', async () => { @@ -62,7 +68,9 @@ describe(ActivityController.name, () => { .post('/activities') .send({ albumId: factory.uuid(), type: 'comment', comment: null }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty'])); + expect(body).toEqual( + factory.responses.badRequest(expect.arrayContaining(['Invalid input: expected string, received null'])), + ); }); }); @@ -75,7 +83,7 @@ describe(ActivityController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['Invalid UUID']))); }); }); }); diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index 850e95510f746..5bd98a4d10ae1 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -5,6 +5,7 @@ import { Endpoint, HistoryBuilder } from 'src/decorators'; import { ActivityCreateDto, ActivityDto, + ActivityIdParamDto, ActivityResponseDto, ActivitySearchDto, ActivityStatisticsResponseDto, @@ -13,7 +14,6 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { ActivityService } from 'src/services/activity.service'; -import { UUIDParamDto } from 'src/validation'; @ApiTags(ApiTag.Activities) @Controller('activities') @@ -70,7 +70,7 @@ export class ActivityController { description: 'Removes a like or comment from a given album or asset in an album.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + deleteActivity(@Auth() auth: AuthDto, @Param() { id }: ActivityIdParamDto): Promise { return this.service.delete(auth, id); } } diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 8629b6c799563..774cf49458fdc 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -47,9 +47,13 @@ describe(MemoryController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), - ); + expect(body).toMatchObject({ + error: 'Bad Request', + statusCode: 400, + correlationId: expect.any(String), + }); + expect(Array.isArray(body.message)).toBe(true); + expect(body.message.length).toBeGreaterThan(0); }); }); diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index edda974476dc8..cd34413082ce7 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -77,7 +77,9 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['Invalid input: expected int, received number'])), + ); }); it(`should not allow decimal quota`, async () => { @@ -93,7 +95,9 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['Invalid input: expected int, received number'])), + ); }); }); @@ -116,7 +120,9 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send({ quotaSizeInBytes: 1.2 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['Invalid input: expected int, received number'])), + ); }); it('should allow a null pinCode', async () => { diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 6464d88508cb6..4e445e58305e7 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,84 +1,116 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Activity } from 'src/database'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; - -export enum ReactionType { - COMMENT = 'comment', - LIKE = 'like', -} +import { UserResponseSchema } from 'src/dtos/user.dto'; +import { UserAvatarColor } from 'src/enum'; +import { z } from 'zod'; export enum ReactionLevel { ALBUM = 'album', ASSET = 'asset', } +const ReactionLevelSchema = z.enum(ReactionLevel).describe('Reaction level').meta({ id: 'ReactionLevel' }); -export type MaybeDuplicate = { duplicate: boolean; value: T }; - -export class ActivityResponseDto { - @ApiProperty({ description: 'Activity ID' }) - id!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: Date; - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type' }) - type!: ReactionType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ApiProperty({ description: 'Asset ID (if activity is for an asset)' }) - assetId!: string | null; - @ApiPropertyOptional({ description: 'Comment text (for comment activities)' }) - comment?: string | null; +export enum ReactionType { + COMMENT = 'comment', + LIKE = 'like', } +const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' }); -export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of comments' }) - comments!: number; +export type MaybeDuplicate = { duplicate: boolean; value: T }; - @ApiProperty({ type: 'integer', description: 'Number of likes' }) - likes!: number; -} +export const ActivityResponseSchema = z + .object({ + id: z.uuidv4().describe('Activity ID'), + createdAt: z.iso.datetime().describe('Creation date'), + user: UserResponseSchema, + assetId: z.uuidv4().describe('Asset ID (if activity is for an asset)').nullable(), + type: ReactionTypeSchema, + comment: z.string().describe('Comment text (for comment activities)').nullish(), + }) + .meta({ id: 'ActivityResponseDto' }); -export class ActivityDto { - @ValidateUUID({ description: 'Album ID' }) - albumId!: string; +export const ActivityStatisticsResponseSchema = z + .object({ + comments: z.int().min(0).describe('Number of comments'), + likes: z.int().min(0).describe('Number of likes'), + }) + .meta({ id: 'ActivityStatisticsResponseDto' }); - @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) - assetId?: string; -} +const ActivityIdParamSchema = z + .object({ + id: z.uuidv4().describe('Activity ID'), + }) + .meta({ id: 'ActivityIdParamDto' }); -export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) - type?: ReactionType; +const ActivitySchema = z + .object({ + albumId: z.uuidv4().describe('Album ID'), + assetId: z.uuidv4().describe('Asset ID (if activity is for an asset)').optional(), + }) + .describe('Activity'); - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) - level?: ReactionLevel; - - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const ActivitySearchSchema = ActivitySchema.extend({ + type: ReactionTypeSchema.optional().describe('Filter by activity type'), + level: ReactionLevelSchema.optional().describe('Filter by activity level'), + userId: z.uuidv4().describe('Filter by user ID').optional(), +}).describe('Activity search'); -const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; +const ActivityCreateSchema = ActivitySchema.extend({ + type: ReactionTypeSchema, + assetId: z.uuidv4().describe('Asset ID (if activity is for an asset)').optional(), + comment: z.string().describe('Comment text (required if type is comment)').optional(), +}) + .refine((data) => data.type !== ReactionType.COMMENT || (data.comment && data.comment.trim() !== ''), { + message: 'Comment is required when type is COMMENT', + path: ['comment'], + }) + .refine((data) => data.type === ReactionType.COMMENT || !data.comment, { + message: 'Comment must not be provided when type is not COMMENT', + path: ['comment'], + }) + .describe('Activity create'); -export class ActivityCreateDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' }) - type!: ReactionType; +export const mapActivity = (activity: Activity): ActivityResponseDto => { + const type = activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT; - @ApiPropertyOptional({ description: 'Comment text (required if type is comment)' }) - @ValidateIf(isComment) - @IsNotEmpty() - @IsString() - comment?: string; -} + if (type === ReactionType.COMMENT) { + return { + id: activity.id, + assetId: activity.assetId, + createdAt: new Date(activity.createdAt).toISOString(), + type: ReactionType.COMMENT, + user: { + id: activity.user.id, + name: activity.user.name, + email: activity.user.email, + profileImagePath: activity.user.profileImagePath, + avatarColor: activity.user.avatarColor ?? UserAvatarColor.Primary, + profileChangedAt: new Date(activity.user.profileChangedAt).toISOString(), + }, + comment: activity.comment, + }; + } -export const mapActivity = (activity: Activity): ActivityResponseDto => { return { id: activity.id, assetId: activity.assetId, - createdAt: activity.createdAt, - comment: activity.comment, - type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT, - user: mapUser(activity.user), + createdAt: activity.createdAt.toISOString(), + type: ReactionType.LIKE, + user: { + id: activity.user.id, + name: activity.user.name, + email: activity.user.email, + profileImagePath: activity.user.profileImagePath, + avatarColor: activity.user.avatarColor ?? UserAvatarColor.Primary, + profileChangedAt: new Date(activity.user.profileChangedAt).toISOString(), + }, + comment: null, }; }; + +export class ActivityResponseDto extends createZodDto(ActivityResponseSchema) {} +export class ActivityCreateDto extends createZodDto(ActivityCreateSchema) {} +export class ActivityIdParamDto extends createZodDto(ActivityIdParamSchema) {} +export class ActivityDto extends createZodDto(ActivitySchema) {} +export class ActivitySearchDto extends createZodDto(ActivitySearchSchema) {} +export class ActivityStatisticsResponseDto extends createZodDto(ActivityStatisticsResponseSchema) {} diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index d3536a3482e69..d4b50e3145b20 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -7,8 +7,8 @@ describe('mapAlbum', () => { const endDate = new Date('2025-01-01T01:02:03.456Z'); const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build(); const dto = mapAlbum(album, false); - expect(dto.startDate).toEqual(startDate); - expect(dto.endDate).toEqual(endDate); + expect(dto.startDate).toEqual(startDate.toISOString()); + expect(dto.endDate).toEqual(endDate.toISOString()); }); it('should not set start and end dates for empty assets', () => { diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 62013fbd92b6f..2696799632c85 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -2,13 +2,15 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; +import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AlbumUserRole, AssetOrder } from 'src/enum'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; export class AlbumInfoDto { @ValidateBoolean({ optional: true, description: 'Exclude assets from response' }) @@ -126,68 +128,47 @@ export class UpdateAlbumUserDto { role!: AlbumUserRole; } -export class AlbumUserResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -export class ContributorCountResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; +export const AlbumUserResponseSchema = z + .object({ + user: UserResponseSchema, + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) - assetCount!: number; -} +export class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} -export class AlbumResponseDto { - @ApiProperty({ description: 'Album ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Album name' }) - albumName!: string; - @ApiProperty({ description: 'Album description' }) - description!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'Thumbnail asset ID' }) - albumThumbnailAssetId!: string | null; - @ApiProperty({ description: 'Is shared album' }) - shared!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albumUsers!: AlbumUserResponseDto[]; - @ApiProperty({ description: 'Has shared link' }) - hasSharedLink!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - owner!: UserResponseDto; - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assetCount!: number; - @ApiPropertyOptional({ description: 'Last modified asset timestamp' }) - lastModifiedAssetTimestamp?: Date; - @ApiPropertyOptional({ description: 'Start date (earliest asset)' }) - startDate?: Date; - @ApiPropertyOptional({ description: 'End date (latest asset)' }) - endDate?: Date; - @ApiProperty({ description: 'Activity feed enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; +export const ContributorCountResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + assetCount: z.int().min(0).describe('Number of assets contributed'), + }) + .meta({ id: 'ContributorCountResponseDto' }); + +export const AlbumResponseSchema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner user ID'), + albumName: z.string().describe('Album name'), + description: z.string().describe('Album description'), + createdAt: z.iso.datetime().describe('Creation date'), + updatedAt: z.iso.datetime().describe('Last update date'), + albumThumbnailAssetId: z.string().describe('Thumbnail asset ID').nullable(), + shared: z.boolean().describe('Is shared album'), + albumUsers: z.array(AlbumUserResponseSchema), + hasSharedLink: z.boolean().describe('Has shared link'), + assets: z.array(AssetResponseSchema), + owner: UserResponseSchema, + assetCount: z.int().min(0).describe('Number of assets'), + lastModifiedAssetTimestamp: z.iso.datetime().optional().describe('Last modified asset timestamp'), + startDate: z.iso.datetime().optional().describe('Start date (earliest asset)'), + endDate: z.iso.datetime().optional().describe('End date (latest asset)'), + isActivityEnabled: z.boolean().describe('Activity feed enabled'), + order: AssetOrderSchema.optional(), + contributorCounts: z.array(ContributorCountResponseSchema).optional(), + }) + .meta({ id: 'AlbumResponseDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Type(() => ContributorCountResponseDto) - contributorCounts?: ContributorCountResponseDto[]; -} +export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} export type MapAlbumDto = { albumUsers?: AlbumUser[]; @@ -210,9 +191,8 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt if (entity.albumUsers) { for (const albumUser of entity.albumUsers) { - const user = mapUser(albumUser.user); albumUsers.push({ - user, + user: mapUser(albumUser.user), role: albumUser.role, }); } @@ -236,16 +216,16 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt albumName: entity.albumName, description: entity.description, albumThumbnailAssetId: entity.albumThumbnailAssetId, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, + createdAt: new Date(entity.createdAt).toISOString(), + updatedAt: new Date(entity.updatedAt).toISOString(), id: entity.id, ownerId: entity.ownerId, owner: mapUser(entity.owner), albumUsers: albumUsersSorted, shared: hasSharedUser || hasSharedLink, hasSharedLink, - startDate, - endDate, + startDate: startDate == null ? undefined : new Date(startDate).toISOString(), + endDate: endDate == null ? undefined : new Date(endDate).toISOString(), assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index df02a0cdea2a4..7550ddc7e0c3d 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,140 +1,99 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Selectable } from 'kysely'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { ExifResponseSchema, mapExif } from 'src/dtos/exif.dto'; import { - AssetFaceWithoutPersonResponseDto, + AssetFaceWithoutPersonResponseSchema, PersonWithFacesResponseDto, + PersonWithFacesResponseSchema, mapFacesWithoutPerson, mapPerson, } from 'src/dtos/person.dto'; -import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { TagResponseSchema, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { AssetStatus, AssetType, AssetTypeSchema, AssetVisibility, AssetVisibilitySchema } from 'src/enum'; import { ImageDimensions } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; - -export class SanitizedAssetResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ description: 'Thumbhash for thumbnail generation' }) - thumbhash!: string | null; - @ApiPropertyOptional({ description: 'Original MIME type' }) - originalMimeType?: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - 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-15T14:30:00.000Z', +import { z } from 'zod'; + +const SanitizedAssetResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + type: AssetTypeSchema, + thumbhash: z.string().describe('Thumbhash for thumbnail generation').nullable(), + originalMimeType: z.string().optional().describe('Original MIME type'), + localDateTime: z.iso + .datetime() + .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().describe('Video duration (for videos)'), + livePhotoVideoId: z.string().describe('Live photo video ID').nullish(), + hasMetadata: z.boolean().describe('Whether asset has metadata'), + width: z.number().min(0).describe('Asset width').nullable(), + height: z.number().min(0).describe('Asset height').nullable(), }) - localDateTime!: Date; - @ApiProperty({ description: 'Video duration (for videos)' }) - duration!: string; - @ApiPropertyOptional({ description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; - @ApiProperty({ description: 'Whether asset has metadata' }) - hasMetadata!: boolean; - @ApiProperty({ description: 'Asset width' }) - width!: number | null; - @ApiProperty({ description: 'Asset height' }) - height!: number | null; -} + .meta({ id: 'SanitizedAssetResponseDto' }); -export class AssetResponseDto extends SanitizedAssetResponseDto { - @ApiProperty({ - type: 'string', - format: 'date-time', - description: 'The UTC timestamp when the asset was originally uploaded to Immich.', - example: '2024-01-15T20:30:00.000Z', - }) - createdAt!: Date; - @ApiProperty({ description: 'Device asset ID' }) - deviceAssetId!: string; - @ApiProperty({ description: 'Device ID' }) - deviceId!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - owner?: UserResponseDto; - @ValidateUUID({ - nullable: true, - description: 'Library ID', - history: new HistoryBuilder().added('v1').deprecated('v1'), - }) - libraryId?: string | null; - @ApiProperty({ description: 'Original file path' }) - originalPath!: string; - @ApiProperty({ description: 'Original file name' }) - originalFileName!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - 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-15T19:30:00.000Z', - }) - fileCreatedAt!: Date; - @ApiProperty({ - type: 'string', - format: 'date-time', - 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-16T10:15:00.000Z', - }) - fileModifiedAt!: Date; - @ApiProperty({ - type: 'string', - format: 'date-time', - 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-16T12:45:30.000Z', - }) - updatedAt!: Date; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ApiProperty({ description: 'Is archived' }) - isArchived!: boolean; - @ApiProperty({ description: 'Is trashed' }) - isTrashed!: boolean; - @ApiProperty({ description: 'Is offline' }) - isOffline!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) - visibility!: AssetVisibility; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - exifInfo?: ExifResponseDto; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - tags?: TagResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - people?: PersonWithFacesResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - @ApiProperty({ description: 'Base64 encoded SHA1 hash' }) - checksum!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - stack?: AssetStackResponseDto | null; - @ApiPropertyOptional({ description: 'Duplicate group ID' }) - duplicateId?: string | null; +export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} - @Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) - resized?: boolean; - @Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) - isEdited!: boolean; -} +const AssetStackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assetCount: z.int().min(0).describe('Number of assets in stack'), + }) + .meta({ id: 'AssetStackResponseDto' }); + +export class AssetStackResponseDto extends createZodDto(AssetStackResponseSchema) {} + +export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( + z.object({ + createdAt: z.iso.datetime().describe('The UTC timestamp when the asset was originally uploaded to Immich.'), + deviceAssetId: z.string().describe('Device asset ID'), + deviceId: z.string().describe('Device ID'), + ownerId: z.string().describe('Owner user ID'), + owner: UserResponseSchema.optional(), + libraryId: z.uuidv4().describe('Library ID').nullish(), + originalPath: z.string().describe('Original file path'), + originalFileName: z.string().describe('Original file name'), + fileCreatedAt: z.iso + .datetime() + .describe( + '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.', + ), + fileModifiedAt: z.iso + .datetime() + .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.iso + .datetime() + .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'), + isArchived: z.boolean().describe('Is archived'), + isTrashed: z.boolean().describe('Is trashed'), + isOffline: z.boolean().describe('Is offline'), + visibility: AssetVisibilitySchema, + exifInfo: ExifResponseSchema.optional(), + tags: z.array(TagResponseSchema).optional(), + people: z.array(PersonWithFacesResponseSchema).optional(), + unassignedFaces: z.array(AssetFaceWithoutPersonResponseSchema).optional(), + checksum: z.string().describe('Base64 encoded SHA1 hash'), + stack: AssetStackResponseSchema.nullish(), + duplicateId: z.string().describe('Duplicate group ID').nullish(), + resized: z.boolean().optional().describe('Is resized'), + isEdited: z.boolean().describe('Is edited'), + }).shape, +).meta({ id: 'AssetResponseDto' }); + +export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export type MapAsset = { createdAt: Date; @@ -176,17 +135,6 @@ export type MapAsset = { isEdited: boolean; }; -export class AssetStackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets in stack' }) - assetCount!: number; -} - export type AssetMapOptions = { stripMetadata?: boolean; withStack?: boolean; @@ -240,7 +188,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset type: entity.type, originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, - localDateTime: entity.localDateTime, + localDateTime: new Date(entity.localDateTime).toISOString(), duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -254,7 +202,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset return { id: entity.id, - createdAt: entity.createdAt, + createdAt: new Date(entity.createdAt).toISOString(), deviceAssetId: entity.deviceAssetId, ownerId: entity.ownerId, owner: entity.owner ? mapUser(entity.owner) : undefined, @@ -265,10 +213,10 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, - fileCreatedAt: entity.fileCreatedAt, - fileModifiedAt: entity.fileModifiedAt, - localDateTime: entity.localDateTime, - updatedAt: entity.updatedAt, + fileCreatedAt: new Date(entity.fileCreatedAt).toISOString(), + fileModifiedAt: new Date(entity.fileModifiedAt).toISOString(), + localDateTime: new Date(entity.localDateTime).toISOString(), + updatedAt: new Date(entity.updatedAt).toISOString(), isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite, isArchived: entity.visibility === AssetVisibility.Archive, isTrashed: !!entity.deletedAt, diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 9cd9147ec5ca7..76d5bb81de61f 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,9 +1,12 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { z } from 'zod'; -export class DuplicateResponseDto { - @ApiProperty({ description: 'Duplicate group ID' }) - duplicateId!: string; - @ApiProperty({ description: 'Duplicate assets' }) - assets!: AssetResponseDto[]; -} +export const DuplicateResponseSchema = z + .object({ + duplicateId: z.string().describe('Duplicate group ID'), + assets: z.array(AssetResponseSchema), + }) + .meta({ id: 'DuplicateResponseDto' }); + +export class DuplicateResponseDto extends createZodDto(DuplicateResponseSchema) {} diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 0052b95b6ec9b..f42b98472b036 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,53 +1,36 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { Exif } from 'src/database'; +import { z } from 'zod'; -export class ExifResponseDto { - @ApiPropertyOptional({ description: 'Camera make' }) - make?: string | null = null; - @ApiPropertyOptional({ description: 'Camera model' }) - model?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Image width in pixels' }) - exifImageWidth?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Image height in pixels' }) - exifImageHeight?: number | null = null; +export const ExifResponseSchema = z + .object({ + make: z.string().describe('Camera make').nullish(), + model: z.string().describe('Camera model').nullish(), + exifImageWidth: z.int().min(0).describe('Image width in pixels').nullish(), + exifImageHeight: z.int().min(0).describe('Image height in pixels').nullish(), + fileSizeInByte: z.int().min(0).describe('File size in bytes').nullish(), + orientation: z.string().describe('Image orientation').nullish(), + dateTimeOriginal: z.iso.datetime().describe('Original date/time').nullish(), + modifyDate: z.iso.datetime().describe('Modification date/time').nullish(), + timeZone: z.string().describe('Time zone').nullish(), + lensModel: z.string().describe('Lens model').nullish(), + fNumber: z.number().describe('F-number (aperture)').nullish(), + focalLength: z.number().describe('Focal length in mm').nullish(), + iso: z.number().describe('ISO sensitivity').nullish(), + exposureTime: z.string().describe('Exposure time').nullish(), + latitude: z.number().describe('GPS latitude').nullish(), + longitude: z.number().describe('GPS longitude').nullish(), + city: z.string().describe('City name').nullish(), + state: z.string().describe('State/province name').nullish(), + country: z.string().describe('Country name').nullish(), + description: z.string().describe('Image description').nullish(), + projectionType: z.string().describe('Projection type').nullish(), + rating: z.number().describe('Rating').nullish(), + }) + .describe('EXIF response') + .meta({ id: 'ExifResponseDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'File size in bytes' }) - fileSizeInByte?: number | null = null; - @ApiPropertyOptional({ description: 'Image orientation' }) - orientation?: string | null = null; - @ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' }) - dateTimeOriginal?: Date | null = null; - @ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' }) - modifyDate?: Date | null = null; - @ApiPropertyOptional({ description: 'Time zone' }) - timeZone?: string | null = null; - @ApiPropertyOptional({ description: 'Lens model' }) - lensModel?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'F-number (aperture)' }) - fNumber?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Focal length in mm' }) - focalLength?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'ISO sensitivity' }) - iso?: number | null = null; - @ApiPropertyOptional({ description: 'Exposure time' }) - exposureTime?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'GPS latitude' }) - latitude?: number | null = null; - @ApiPropertyOptional({ type: 'number', description: 'GPS longitude' }) - longitude?: number | null = null; - @ApiPropertyOptional({ description: 'City name' }) - city?: string | null = null; - @ApiPropertyOptional({ description: 'State/province name' }) - state?: string | null = null; - @ApiPropertyOptional({ description: 'Country name' }) - country?: string | null = null; - @ApiPropertyOptional({ description: 'Image description' }) - description?: string | null = null; - @ApiPropertyOptional({ description: 'Projection type' }) - projectionType?: string | null = null; - @ApiPropertyOptional({ type: 'number', description: 'Rating' }) - rating?: number | null = null; -} +export class ExifResponseDto extends createZodDto(ExifResponseSchema) {} export function mapExif(entity: Exif): ExifResponseDto { return { @@ -57,8 +40,8 @@ export function mapExif(entity: Exif): ExifResponseDto { exifImageHeight: entity.exifImageHeight, fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, - dateTimeOriginal: entity.dateTimeOriginal, - modifyDate: entity.modifyDate, + dateTimeOriginal: entity.dateTimeOriginal == null ? null : new Date(entity.dateTimeOriginal).toISOString(), + modifyDate: entity.modifyDate == null ? null : new Date(entity.modifyDate).toISOString(), timeZone: entity.timeZone, lensModel: entity.lensModel, fNumber: entity.fNumber, @@ -80,7 +63,7 @@ export function mapSanitizedExif(entity: Exif): ExifResponseDto { return { fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, - dateTimeOriginal: entity.dateTimeOriginal, + dateTimeOriginal: entity.dateTimeOriginal == null ? null : new Date(entity.dateTimeOriginal).toISOString(), timeZone: entity.timeZone, projectionType: entity.projectionType, exifImageWidth: entity.exifImageWidth, diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 0d73c19b2012b..3103536785967 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { IsInt, IsPositive } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Memory } from 'src/database'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetOrderWithRandom, MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { AssetOrderWithRandom, MemoryType, MemoryTypeSchema } from 'src/enum'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum } from 'src/validation'; +import { z } from 'zod'; class MemoryBaseDto { @ValidateBoolean({ optional: true, description: 'Is memory saved' }) @@ -39,12 +41,20 @@ export class MemorySearchDto { order?: AssetOrderWithRandom; } -class OnThisDayDto { - @ApiProperty({ type: 'number', description: 'Year for on this day memory', minimum: 1 }) - @IsInt() - @IsPositive() - year!: number; -} +export const OnThisDaySchema = z + .object({ + year: z + .int() + .min(1) + .max(9999) + .refine((val) => /^\d{4}$/.test(String(val)), { + message: 'Year must be exactly 4 digits', + }) + .describe('Year for on this day memory'), + }) + .meta({ id: 'OnThisDayDto' }); + +class OnThisDayDto extends createZodDto(OnThisDaySchema) {} type MemoryData = OnThisDayDto; @@ -53,80 +63,54 @@ export class MemoryUpdateDto extends MemoryBaseDto { memoryAt?: Date; } -export class MemoryCreateDto extends MemoryBaseDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @IsObject() - @ValidateNested() - @Type((options) => { - switch (options?.object.type) { - case MemoryType.OnThisDay: { - return OnThisDayDto; - } - - default: { - return Object; - } - } +export const MemoryCreateSchema = z + .object({ + type: MemoryTypeSchema.describe('Memory type'), + data: OnThisDaySchema.describe('Memory type-specific data'), + memoryAt: z.iso.datetime().describe('Memory date'), + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to associate with memory'), + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: z.iso.datetime().optional().describe('Date when memory was seen'), }) - data!: MemoryData; - - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; + .meta({ id: 'MemoryCreateDto' }); - @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) - assetIds?: string[]; -} +export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {} export class MemoryStatisticsResponseDto { @ApiProperty({ type: 'integer', description: 'Total number of memories' }) total!: number; } -export class MemoryResponseDto { - @ApiProperty({ description: 'Memory ID' }) - id!: string; - @ValidateDate({ description: 'Creation date' }) - createdAt!: Date; - @ValidateDate({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateDate({ optional: true, description: 'Deletion date' }) - deletedAt?: Date; - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be shown' }) - showAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be hidden' }) - hideAt?: Date; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: MemoryData; - @ApiProperty({ description: 'Is memory saved' }) - isSaved!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; -} +export const MemoryResponseSchema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: z.iso.datetime().describe('Creation date'), + updatedAt: z.iso.datetime().describe('Last update date'), + deletedAt: z.iso.datetime().optional().describe('Deletion date'), + memoryAt: z.iso.datetime().describe('Memory date'), + seenAt: z.iso.datetime().optional().describe('Date when memory was seen'), + showAt: z.iso.datetime().optional().describe('Date when memory should be shown'), + hideAt: z.iso.datetime().optional().describe('Date when memory should be hidden'), + ownerId: z.string().describe('Owner user ID'), + type: MemoryTypeSchema, + data: OnThisDaySchema, + isSaved: z.boolean().describe('Is memory saved'), + assets: z.array(AssetResponseSchema), + }) + .meta({ id: 'MemoryResponseDto' }); + +export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {} export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { return { id: entity.id, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - deletedAt: entity.deletedAt ?? undefined, - memoryAt: entity.memoryAt, - seenAt: entity.seenAt ?? undefined, - showAt: entity.showAt ?? undefined, - hideAt: entity.hideAt ?? undefined, + createdAt: new Date(entity.createdAt).toISOString(), + updatedAt: new Date(entity.updatedAt).toISOString(), + deletedAt: entity.deletedAt == null ? undefined : new Date(entity.deletedAt).toISOString(), + memoryAt: new Date(entity.memoryAt).toISOString(), + seenAt: entity.seenAt == null ? undefined : new Date(entity.seenAt).toISOString(), + showAt: entity.showAt == null ? undefined : new Date(entity.showAt).toISOString(), + hideAt: entity.hideAt == null ? undefined : new Date(entity.hideAt).toISOString(), ownerId: entity.ownerId, type: entity.type as MemoryType, data: entity.data as unknown as MemoryData, diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 5b949326a4a5e..5ef0c93c20cbf 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,8 +1,10 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; -import { UserResponseDto } from 'src/dtos/user.dto'; +import { createZodDto } from 'nestjs-zod'; +import { UserResponseSchema } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; export class PartnerCreateDto { @ValidateUUID({ description: 'User ID to share with' }) @@ -20,7 +22,10 @@ export class PartnerSearchDto { direction!: PartnerDirection; } -export class PartnerResponseDto extends UserResponseDto { - @ApiPropertyOptional({ description: 'Show in timeline' }) - inTimeline?: boolean; -} +export const PartnerResponseSchema = UserResponseSchema.extend({ + inTimeline: z.boolean().optional().describe('Show in timeline'), +}) + .describe('Partner response') + .meta({ id: 'PartnerResponseDto' }); + +export class PartnerResponseDto extends createZodDto(PartnerResponseSchema) {} diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 983062afcf798..be9870b1c031a 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -3,11 +3,11 @@ import { Type } from 'class-transformer'; import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; import { Selectable } from 'kysely'; import { DateTime } from 'luxon'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, Person } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { SourceType } from 'src/enum'; +import { SourceTypeSchema } from 'src/enum'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { ImageDimensions } from 'src/types'; import { asDateString } from 'src/utils/date'; @@ -17,10 +17,10 @@ import { MaxDateString, Optional, ValidateBoolean, - ValidateEnum, ValidateHexColor, ValidateUUID, } from 'src/validation'; +import { z } from 'zod'; export class PersonCreateDto { @ApiPropertyOptional({ description: 'Person name' }) @@ -94,53 +94,48 @@ export class PersonSearchDto { size: number = 500; } -export class PersonResponseDto { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ format: 'date', description: 'Person date of birth' }) - birthDate!: string | null; - @ApiProperty({ description: 'Thumbnail path' }) - thumbnailPath!: string; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') }) - updatedAt?: Date; - @Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - isFavorite?: boolean; - @Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - color?: string; -} +export const PersonResponseSchema = z + .object({ + id: z.string().describe('Person ID'), + name: z.string().describe('Person name'), + birthDate: z.iso.date().describe('Person date of birth').nullable(), + thumbnailPath: z.string().describe('Thumbnail path'), + isHidden: z.boolean().describe('Is hidden'), + updatedAt: z.iso.datetime().optional().describe('Last update date'), + isFavorite: z.boolean().optional().describe('Is favorite'), + color: z.string().optional().describe('Person color (hex)'), + }) + .meta({ id: 'PersonResponseDto' }); + +export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} + +export const AssetFaceWithoutPersonResponseSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + imageHeight: z.int().min(0).describe('Image height in pixels'), + imageWidth: z.int().min(0).describe('Image width in pixels'), + boundingBoxX1: z.int().describe('Bounding box X1 coordinate'), + boundingBoxX2: z.int().describe('Bounding box X2 coordinate'), + boundingBoxY1: z.int().describe('Bounding box Y1 coordinate'), + boundingBoxY2: z.int().describe('Bounding box Y2 coordinate'), + sourceType: SourceTypeSchema.optional().describe('Face detection source type'), + }) + .describe('Asset face without person') + .meta({ id: 'AssetFaceWithoutPersonResponseDto' }); -export class PersonWithFacesResponseDto extends PersonResponseDto { - @ApiProperty({ description: 'Face detections' }) - faces!: AssetFaceWithoutPersonResponseDto[]; -} +export class AssetFaceWithoutPersonResponseDto extends createZodDto(AssetFaceWithoutPersonResponseSchema) {} -export class AssetFaceWithoutPersonResponseDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - imageHeight!: number; - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - imageWidth!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' }) - boundingBoxY2!: number; - @ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' }) - sourceType?: SourceType; -} +export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({ + faces: z.array(AssetFaceWithoutPersonResponseSchema), +}).meta({ id: 'PersonWithFacesResponseDto' }); -export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { - @ApiProperty({ description: 'Person associated with face' }) - person!: PersonResponseDto | null; -} +export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} + +export const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ + person: PersonResponseSchema.nullable(), +}).meta({ id: 'AssetFaceResponseDto' }); + +export class AssetFaceResponseDto extends createZodDto(AssetFaceResponseSchema) {} export class AssetFaceUpdateDto { @ApiProperty({ description: 'Face update items' }) @@ -206,21 +201,15 @@ export class PersonStatisticsResponseDto { assets!: number; } -export class PeopleResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of people' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of hidden people' }) - hidden!: number; - @ApiProperty({ description: 'List of people' }) - people!: PersonResponseDto[]; - - // TODO: make required after a few versions - @Property({ - description: 'Whether there are more pages', - history: new HistoryBuilder().added('v1.110.0').stable('v2'), +const PeopleResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of people'), + hidden: z.int().min(0).describe('Number of hidden people'), + people: z.array(PersonResponseSchema), + hasNextPage: z.boolean().optional().describe('Whether there are more pages'), }) - hasNextPage?: boolean; -} + .describe('People response'); +export class PeopleResponseDto extends createZodDto(PeopleResponseSchema) {} export function mapPerson(person: Person): PersonResponseDto { return { @@ -231,7 +220,7 @@ export function mapPerson(person: Person): PersonResponseDto { isHidden: person.isHidden, isFavorite: person.isFavorite, color: person.color ?? undefined, - updatedAt: person.updatedAt, + updatedAt: person.updatedAt == null ? undefined : new Date(person.updatedAt).toISOString(), }; } diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index de1f1b28d43af..fd6a1e99cb88a 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -100,8 +100,8 @@ export function mapPlugin(plugin: MapPlugin): PluginResponseDto { description: plugin.description, author: plugin.author, version: plugin.version, - createdAt: plugin.createdAt.toISOString(), - updatedAt: plugin.updatedAt.toISOString(), + createdAt: new Date(plugin.createdAt).toISOString(), + updatedAt: new Date(plugin.updatedAt).toISOString(), filters: plugin.filters, actions: plugin.actions, }; diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 47a1889e4778c..0e2e10c88bf78 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,12 +1,14 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Place } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AlbumResponseSchema } from 'src/dtos/album.dto'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { z } from 'zod'; class BaseSearchDto { @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) @@ -325,77 +327,78 @@ export class SearchSuggestionRequestDto { includeNull?: boolean; } -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' }) - count!: number; - @ApiProperty({ description: 'Facet value' }) - value!: string; -} +export const SearchFacetCountResponseSchema = z + .object({ + count: z.int().min(0).describe('Number of assets with this facet value'), + value: z.string().describe('Facet value'), + }) + .meta({ id: 'SearchFacetCountResponseDto' }); -class SearchFacetResponseDto { - @ApiProperty({ description: 'Facet field name' }) - fieldName!: string; - @ApiProperty({ description: 'Facet counts' }) - counts!: SearchFacetCountResponseDto[]; -} +export class SearchFacetCountResponseDto extends createZodDto(SearchFacetCountResponseSchema) {} -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching albums' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of albums in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AlbumResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; -} +export const SearchFacetResponseSchema = z + .object({ + fieldName: z.string().describe('Facet field name'), + counts: z.array(SearchFacetCountResponseSchema), + }) + .meta({ id: 'SearchFacetResponseDto' }); -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of assets in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; - @ApiProperty({ description: 'Next page token' }) - nextPage!: string | null; -} +export class SearchFacetResponseDto extends createZodDto(SearchFacetResponseSchema) {} -export class SearchResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albums!: SearchAlbumResponseDto; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: SearchAssetResponseDto; -} +export const SearchAlbumResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching albums'), + count: z.int().min(0).describe('Number of albums in this page'), + items: z.array(AlbumResponseSchema), + facets: z.array(SearchFacetResponseSchema), + }) + .meta({ id: 'SearchAlbumResponseDto' }); + +export class SearchAlbumResponseDto extends createZodDto(SearchAlbumResponseSchema) {} + +export const SearchAssetResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching assets'), + count: z.int().min(0).describe('Number of assets in this page'), + items: z.array(AssetResponseSchema), + facets: z.array(SearchFacetResponseSchema), + nextPage: z.string().describe('Next page token').nullable(), + }) + .meta({ id: 'SearchAssetResponseDto' }); + +export class SearchAssetResponseDto extends createZodDto(SearchAssetResponseSchema) {} + +export const SearchResponseSchema = z + .object({ + albums: SearchAlbumResponseSchema, + assets: SearchAssetResponseSchema, + }) + .meta({ id: 'SearchResponseDto' }); + +export class SearchResponseDto extends createZodDto(SearchResponseSchema) {} export class SearchStatisticsResponseDto { @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) total!: number; } -class SearchExploreItem { - @ApiProperty({ description: 'Explore value' }) - value!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: AssetResponseDto; -} +export const SearchExploreItemSchema = z + .object({ + value: z.string().describe('Explore value'), + data: AssetResponseSchema, + }) + .meta({ id: 'SearchExploreItem' }); -export class SearchExploreResponseDto { - @ApiProperty({ description: 'Explore field name' }) - fieldName!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: SearchExploreItem[]; -} +export class SearchExploreItem extends createZodDto(SearchExploreItemSchema) {} + +export const SearchExploreResponseSchema = z + .object({ + fieldName: z.string().describe('Explore field name'), + items: z.array(SearchExploreItemSchema), + }) + .meta({ id: 'SearchExploreResponseDto' }); + +export class SearchExploreResponseDto extends createZodDto(SearchExploreResponseSchema) {} export class MemoryLaneDto { @ApiProperty({ type: 'integer', description: 'Day of month' }) diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index b2ecc70a3aecc..fbbcb51c174af 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,11 +1,13 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkType } from 'src/enum'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; +import { SharedLinkType, SharedLinkTypeSchema } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { z } from 'zod'; export class SharedLinkSearchDto { @ValidateUUID({ optional: true, description: 'Filter by album ID' }) @@ -110,46 +112,28 @@ export class SharedLinkPasswordDto { @Optional() token?: string; } -export class SharedLinkResponseDto { - @ApiProperty({ description: 'Shared link ID' }) - id!: string; - @ApiProperty({ description: 'Link description' }) - description!: string | null; - @ApiProperty({ description: 'Has password' }) - password!: string | null; - @Property({ - description: 'Access token', - history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), +export const SharedLinkResponseSchema = z + .object({ + id: z.string().describe('Shared link ID'), + description: z.string().describe('Link description').nullable(), + password: z.string().describe('Has password').nullable(), + token: z.string().describe('Access token').nullish(), + userId: z.string().describe('Owner user ID'), + key: z.string().describe('Encryption key (base64url)'), + type: SharedLinkTypeSchema, + createdAt: z.iso.datetime().describe('Creation date'), + expiresAt: z.iso.datetime().describe('Expiration date').nullable(), + assets: z.array(AssetResponseSchema), + album: AlbumResponseSchema.optional(), + allowUpload: z.boolean().describe('Allow uploads'), + allowDownload: z.boolean().describe('Allow downloads'), + showMetadata: z.boolean().describe('Show metadata'), + slug: z.string().describe('Custom URL slug').nullable(), }) - token?: string | null; - @ApiProperty({ description: 'Owner user ID' }) - userId!: string; - @ApiProperty({ description: 'Encryption key (base64url)' }) - key!: string; + .describe('Shared link response') + .meta({ id: 'SharedLinkResponseDto' }); - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Expiration date' }) - expiresAt!: Date | null; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - album?: AlbumResponseDto; - @ApiProperty({ description: 'Allow uploads' }) - allowUpload!: boolean; - - @ApiProperty({ description: 'Allow downloads' }) - allowDownload!: boolean; - @ApiProperty({ description: 'Show metadata' }) - showMetadata!: boolean; - - @ApiProperty({ description: 'Custom URL slug' }) - slug!: string | null; -} +export class SharedLinkResponseDto extends createZodDto(SharedLinkResponseSchema) {} export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { const assets = sharedLink.assets || []; @@ -161,8 +145,8 @@ export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetad userId: sharedLink.userId, key: sharedLink.key.toString('base64url'), type: sharedLink.type, - createdAt: sharedLink.createdAt, - expiresAt: sharedLink.expiresAt, + createdAt: new Date(sharedLink.createdAt).toISOString(), + expiresAt: sharedLink.expiresAt == null ? null : new Date(sharedLink.expiresAt).toISOString(), assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index a76b35e08e73d..617586e3c63be 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,9 +1,10 @@ -import { ApiProperty } from '@nestjs/swagger'; import { ArrayMinSize } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Stack } from 'src/database'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ValidateUUID } from 'src/validation'; +import { z } from 'zod'; export class StackCreateDto { @ValidateUUID({ each: true, description: 'Asset IDs (first becomes primary, min 2)' }) @@ -21,14 +22,16 @@ export class StackUpdateDto { primaryAssetId?: string; } -export class StackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - @ApiProperty({ description: 'Stack assets' }) - assets!: AssetResponseDto[]; -} +export const StackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assets: z.array(AssetResponseSchema), + }) + .describe('Stack response') + .meta({ id: 'StackResponseDto' }); + +export class StackResponseDto extends createZodDto(StackResponseSchema) {} export const mapStack = (stack: Stack, { auth }: { auth?: AuthDto }) => { const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId); diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 59d7d373f011f..bf2948c95ba40 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; import { AlbumUserRole, AssetOrder, @@ -15,6 +16,7 @@ import { } from 'src/enum'; import { UserMetadata } from 'src/types'; import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { z } from 'zod'; export class AssetFullSyncDto { @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) @@ -40,14 +42,16 @@ export class AssetDeltaSyncDto { userIds!: string[]; } -export class AssetDeltaSyncResponseDto { - @ApiProperty({ description: 'Whether full sync is needed' }) - needsFullSync!: boolean; - @ApiProperty({ description: 'Upserted assets' }) - upserted!: AssetResponseDto[]; - @ApiProperty({ description: 'Deleted asset IDs' }) - deleted!: string[]; -} +export const AssetDeltaSyncResponseSchema = z + .object({ + needsFullSync: z.boolean().describe('Whether full sync is needed'), + upserted: z.array(AssetResponseSchema), + deleted: z.array(z.string()).describe('Deleted asset IDs'), + }) + .describe('Asset delta sync response') + .meta({ id: 'AssetDeltaSyncResponseDto' }); + +export class AssetDeltaSyncResponseDto extends createZodDto(AssetDeltaSyncResponseSchema) {} export const extraSyncModels: Function[] = []; diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index bb33659bfe663..f34cbc343c904 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Tag } from 'src/database'; import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; +import { z } from 'zod'; export class TagCreateDto { @ApiProperty({ description: 'Tag name' }) @@ -45,22 +47,19 @@ export class TagBulkAssetsResponseDto { count!: number; } -export class TagResponseDto { - @ApiProperty({ description: 'Tag ID' }) - id!: string; - @ApiPropertyOptional({ description: 'Parent tag ID' }) - parentId?: string; - @ApiProperty({ description: 'Tag name' }) - name!: string; - @ApiProperty({ description: 'Tag value (full path)' }) - value!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - color?: string; -} +export const TagResponseSchema = z + .object({ + id: z.string().describe('Tag ID'), + parentId: z.string().optional().describe('Parent tag ID'), + name: z.string().describe('Tag name'), + value: z.string().describe('Tag value (full path)'), + createdAt: z.iso.datetime().describe('Creation date'), + updatedAt: z.iso.datetime().describe('Last update date'), + color: z.string().optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagResponseDto' }); + +export class TagResponseDto extends createZodDto(TagResponseSchema) {} export function mapTag(entity: Tag): TagResponseDto { return { @@ -68,8 +67,8 @@ export function mapTag(entity: Tag): TagResponseDto { parentId: entity.parentId ?? undefined, name: entity.value.split('/').at(-1) as string, value: entity.value, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, + createdAt: new Date(entity.createdAt).toISOString(), + updatedAt: new Date(entity.updatedAt).toISOString(), color: entity.color ?? undefined, }; } diff --git a/server/src/dtos/user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts index e6be3b17d1173..8a57635e60b68 100644 --- a/server/src/dtos/user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,69 +1,63 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UserAdminCreateDto, UserUpdateMeDto } from 'src/dtos/user.dto'; +import { UserAdminCreateSchema, UserUpdateMeSchema } from 'src/dtos/user.dto'; describe('update user DTO', () => { - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserUpdateMeDto, { + const result = UserUpdateMeSchema.safeParse({ email: someEmail, id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.email).toEqual(someEmail); + } }); }); describe('create user DTO', () => { - it('validates the email', async () => { - const params: Partial = { - email: undefined, - password: 'password', - name: 'name', - }; - let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); - let errors = await validate(dto); - expect(errors).toHaveLength(1); + it('validates the email', () => { + expect(UserAdminCreateSchema.safeParse({ password: 'password', name: 'name' }).success).toBe(false); - params.email = 'invalid email'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(1); + expect( + UserAdminCreateSchema.safeParse({ email: 'invalid email', password: 'password', name: 'name' }).success, + ).toBe(false); - params.email = 'valid@email.com'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(0); - }); - - it('validates invalid email type', async () => { - let dto = plainToInstance(UserAdminCreateDto, { - email: [], - password: 'some password', - name: 'some name', + const result = UserAdminCreateSchema.safeParse({ + email: 'valid@email.com', + password: 'password', + name: 'name', }); - expect(await validate(dto)).toHaveLength(1); + expect(result.success).toBe(true); + }); - dto = plainToInstance(UserAdminCreateDto, { - email: {}, - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + it('validates invalid email type', () => { + expect( + UserAdminCreateSchema.safeParse({ + email: [], + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); + + expect( + UserAdminCreateSchema.safeParse({ + email: {}, + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); }); - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserAdminCreateDto, { + const result = UserAdminCreateSchema.safeParse({ email: someEmail, password: 'some password', name: 'some name', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.email).toEqual(someEmail); + } }); }); diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 2d4fc3934fe74..378458e229d00 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,64 +1,46 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { User, UserAdmin } from 'src/database'; -import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; - -export class UserUpdateMeDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - // TODO: migrate to the other change password endpoint - @ApiPropertyOptional({ description: 'User password (deprecated, use change password endpoint)' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +import { z } from 'zod'; + +export const UserUpdateMeSchema = z + .object({ + email: z.email({ pattern: z.regexes.html5Email }).optional().describe('User email'), + password: z + .string() + .optional() + .describe('User password (deprecated, use change password endpoint)') + .meta({ deprecated: true }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), }) - avatarColor?: UserAvatarColor | null; -} + .meta({ id: 'UserUpdateMeDto' }); + +export class UserUpdateMeDto extends createZodDto(UserUpdateMeSchema) {} + +export const UserResponseSchema = z + .object({ + id: z.uuidv4().describe('User ID'), + name: z.string().describe('User name'), + email: z.string().describe('User email'), + profileImagePath: z.string().describe('Profile image path'), + avatarColor: UserAvatarColorSchema, + profileChangedAt: z.iso.datetime().describe('Profile change date'), + }) + .meta({ id: 'UserResponseDto' }); -export class UserResponseDto { - @ApiProperty({ description: 'User ID' }) - id!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'User email' }) - email!: string; - @ApiProperty({ description: 'Profile image path' }) - profileImagePath!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' }) - avatarColor!: UserAvatarColor; - @ApiProperty({ description: 'Profile change date' }) - profileChangedAt!: Date; -} +export class UserResponseDto extends createZodDto(UserResponseSchema) {} -export class UserLicense { - @ApiProperty({ description: 'License key' }) - licenseKey!: string; - @ApiProperty({ description: 'Activation key' }) - activationKey!: string; - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; + +export const UserLicenseSchema = z + .object({ + licenseKey: z.string().regex(licenseKeyRegex).describe(`License key (format: ${licenseKeyRegex.toString()})`), + activationKey: z.string().describe('Activation key'), + activatedAt: z.iso.datetime().describe('Activation date'), + }) + .meta({ id: 'UserLicense' }); const emailToAvatarColor = (email: string): UserAvatarColor => { const values = Object.values(UserAvatarColor); @@ -75,148 +57,77 @@ export const mapUser = (entity: User | UserAdmin): UserResponseDto => { name: entity.name, profileImagePath: entity.profileImagePath, avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email), - profileChangedAt: entity.profileChangedAt, + profileChangedAt: new Date(entity.profileChangedAt).toISOString(), }; }; -export class UserAdminSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted users' }) - withDeleted?: boolean; - - @ValidateUUID({ optional: true, description: 'User ID filter' }) - id?: string; -} - -export class UserAdminCreateDto { - @ApiProperty({ description: 'User email' }) - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email!: string; - - @ApiProperty({ description: 'User password' }) - @IsString() - password!: string; - - @ApiProperty({ description: 'User name' }) - @IsNotEmpty() - @IsString() - name!: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserAdminSearchSchema = z + .object({ + withDeleted: z.coerce.boolean().optional().describe('Include deleted users'), + id: z.uuidv4().optional().describe('User ID filter'), }) - avatarColor?: UserAvatarColor | null; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true, description: 'Send notification email' }) - notify?: boolean; - - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} - -export class UserAdminUpdateDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - @ApiPropertyOptional({ description: 'User password' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', + .meta({ id: 'UserAdminSearchDto' }); + +export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {} + +const pinCodeRegex = /^\d{6}$/; + +export const UserAdminCreateSchema = z + .object({ + email: z.email({ pattern: z.regexes.html5Email }).describe('User email'), + password: z.string().describe('User password'), + name: z.string().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + pinCode: z.string().regex(pinCodeRegex).describe('PIN code').nullish(), + storageLabel: z.string().describe('Storage label').nullish(), + quotaSizeInBytes: z.int().min(0).describe('Storage quota in bytes').nullish(), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + notify: z.boolean().optional().describe('Send notification email'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), }) - avatarColor?: UserAvatarColor | null; - - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} + .meta({ id: 'UserAdminCreateDto' }); + +export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {} + +export const UserAdminUpdateSchema = z + .object({ + email: z.email({ pattern: z.regexes.html5Email }).optional().describe('User email'), + password: z.string().optional().describe('User password'), + pinCode: z.string().regex(pinCodeRegex).describe('PIN code').nullish(), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + storageLabel: z.string().describe('Storage label').nullish(), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + quotaSizeInBytes: z.int().min(0).describe('Storage quota in bytes').nullish(), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), + }) + .meta({ id: 'UserAdminUpdateDto' }); -export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) - force?: boolean; -} +export class UserAdminUpdateDto extends createZodDto(UserAdminUpdateSchema) {} -export class UserAdminResponseDto extends UserResponseDto { - @ApiProperty({ description: 'Storage label' }) - storageLabel!: string | null; - @ApiProperty({ description: 'Require password change on next login' }) - shouldChangePassword!: boolean; - @ApiProperty({ description: 'Is admin user' }) - isAdmin!: boolean; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Deletion date' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'OAuth ID' }) - oauthId!: string; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - quotaUsageInBytes!: number | null; - @ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' }) - status!: string; - @ApiProperty({ description: 'User license' }) - license!: UserLicense | null; -} +export const UserAdminDeleteSchema = z + .object({ + force: z.boolean().optional().describe('Force delete even if user has assets'), + }) + .meta({ id: 'UserAdminDeleteDto' }); + +export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} + +export const UserAdminResponseSchema = UserResponseSchema.extend({ + storageLabel: z.string().describe('Storage label').nullable(), + shouldChangePassword: z.boolean().describe('Require password change on next login'), + isAdmin: z.boolean().describe('Is admin user'), + createdAt: z.iso.datetime().describe('Creation date'), + deletedAt: z.iso.datetime().describe('Deletion date').nullable(), + updatedAt: z.iso.datetime().describe('Last update date'), + oauthId: z.string().describe('OAuth ID'), + quotaSizeInBytes: z.int().min(0).describe('Storage quota in bytes').nullable(), + quotaUsageInBytes: z.int().min(0).describe('Storage usage in bytes').nullable(), + status: UserStatusSchema, + license: UserLicenseSchema.nullable(), +}).meta({ id: 'UserAdminResponseDto' }); + +export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; @@ -229,13 +140,13 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { storageLabel: entity.storageLabel, shouldChangePassword: entity.shouldChangePassword, isAdmin: entity.isAdmin, - createdAt: entity.createdAt, - deletedAt: entity.deletedAt, - updatedAt: entity.updatedAt, + createdAt: new Date(entity.createdAt).toISOString(), + deletedAt: entity.deletedAt == null ? null : new Date(entity.deletedAt).toISOString(), + updatedAt: new Date(entity.updatedAt).toISOString(), oauthId: entity.oauthId, quotaSizeInBytes: entity.quotaSizeInBytes, quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, - license: license ? { ...license, activatedAt: new Date(license?.activatedAt) } : null, - }; + license: license ? { ...license, activatedAt: new Date(license?.activatedAt).toISOString() } : null, + } as UserAdminResponseDto; } diff --git a/server/src/enum.ts b/server/src/enum.ts index 8f509754dad33..49a605af9b584 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + export enum AuthType { Password = 'password', OAuth = 'oauth', @@ -37,6 +39,8 @@ export enum AssetType { Other = 'OTHER', } +export const AssetTypeSchema = z.enum(AssetType).describe('Asset type').meta({ title: 'AssetTypeEnum' }); + export enum AssetFileType { /** * An full/large-size image extracted/converted from RAW photos @@ -47,16 +51,22 @@ export enum AssetFileType { Sidecar = 'sidecar', } +export const AssetFileTypeSchema = z.enum(AssetFileType).describe('Asset file type').meta({ title: 'AssetFileType' }); + export enum AlbumUserRole { Editor = 'editor', Viewer = 'viewer', } +export const AlbumUserRoleSchema = z.enum(AlbumUserRole).describe('Album user role').meta({ title: 'AlbumUserRole' }); + export enum AssetOrder { Asc = 'asc', Desc = 'desc', } +export const AssetOrderSchema = z.enum(AssetOrder).describe('Asset sort order').meta({ title: 'AssetOrder' }); + export enum DatabaseAction { Create = 'CREATE', Update = 'UPDATE', @@ -73,6 +83,8 @@ export enum MemoryType { OnThisDay = 'on_this_day', } +export const MemoryTypeSchema = z.enum(MemoryType).describe('Memory type').meta({ title: 'MemoryType' }); + export enum AssetOrderWithRandom { // Include existing values Asc = AssetOrder.Asc, @@ -297,6 +309,11 @@ export enum SharedLinkType { Individual = 'INDIVIDUAL', } +export const SharedLinkTypeSchema = z + .enum(SharedLinkType) + .describe('Shared link type') + .meta({ title: 'SharedLinkType' }); + export enum StorageFolder { EncodedVideo = 'encoded-video', Library = 'library', @@ -342,24 +359,32 @@ export enum UserAvatarColor { Amber = 'amber', } +export const UserAvatarColorSchema = z.enum(UserAvatarColor).meta({ title: 'UserAvatarColor' }); + export enum UserStatus { Active = 'active', Removing = 'removing', Deleted = 'deleted', } +export const UserStatusSchema = z.enum(UserStatus).describe('User status').meta({ title: 'UserStatus' }); + export enum AssetStatus { Active = 'active', Trashed = 'trashed', Deleted = 'deleted', } +export const AssetStatusSchema = z.enum(AssetStatus).describe('Asset status').meta({ title: 'AssetStatus' }); + export enum SourceType { MachineLearning = 'machine-learning', Exif = 'exif', Manual = 'manual', } +export const SourceTypeSchema = z.enum(SourceType).describe('Source type').meta({ title: 'SourceType' }); + export enum ManualJobName { PersonCleanup = 'person-cleanup', TagCleanup = 'tag-cleanup', @@ -840,6 +865,11 @@ export enum AssetVisibility { Locked = 'locked', } +export const AssetVisibilitySchema = z + .enum(AssetVisibility) + .describe('Asset visibility') + .meta({ title: 'AssetVisibility' }); + export enum CronJob { LibraryScan = 'LibraryScan', NightlyJobs = 'NightlyJobs', diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts index a8afa91cbcadb..99a1f5030fbf7 100644 --- a/server/src/middleware/global-exception.filter.ts +++ b/server/src/middleware/global-exception.filter.ts @@ -1,8 +1,10 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; import { Response } from 'express'; import { ClsService } from 'nestjs-cls'; +import { ZodSerializationException, ZodValidationException } from 'nestjs-zod'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { logGlobalError } from 'src/utils/logger'; +import { ZodError } from 'zod'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { @@ -41,6 +43,20 @@ export class GlobalExceptionFilter implements ExceptionFilter { body = { message: body }; } + // handle both request and response validation errors + if (error instanceof ZodValidationException || error instanceof ZodSerializationException) { + const zodError = error.getZodError(); + if (zodError instanceof ZodError && zodError.issues.length > 0) { + body = { + message: zodError.issues.map((issue) => issue.message), + // technically more accurate to return 422 Unprocessable Entity here + // nestjs-zod uses 400 Bad Request and we use 400 in v2 + // https://github.com/BenLorantfy/nestjs-zod/issues/328 + error: 'Bad Request', + }; + } + } + return { status, body }; } diff --git a/server/src/middleware/hybrid-validation.pipe.ts b/server/src/middleware/hybrid-validation.pipe.ts new file mode 100644 index 0000000000000..bcefdc822b0c7 --- /dev/null +++ b/server/src/middleware/hybrid-validation.pipe.ts @@ -0,0 +1,17 @@ +import { ArgumentMetadata, Injectable, PipeTransform, ValidationPipe } from '@nestjs/common'; +import { ZodValidationPipe } from 'nestjs-zod'; +import { isZodDto } from 'nestjs-zod/dto'; + +@Injectable() +export class HybridValidationPipe implements PipeTransform { + private readonly zodPipe = new ZodValidationPipe(); + private readonly validationPipe = new ValidationPipe({ transform: true, whitelist: true }); + + transform(value: unknown, metadata: ArgumentMetadata) { + const zodValue = this.zodPipe.transform(value, metadata); + if (metadata.metatype && isZodDto(metadata.metatype)) { + return Object.assign(Object.create(metadata.metatype.prototype), zodValue); + } + return this.validationPipe.transform(zodValue, metadata); + } +} diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 18747dbc3a08f..c93cb0a6341fa 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -64,11 +64,11 @@ export class AlbumService extends BaseService { return albums.map((album) => ({ ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - startDate: albumMetadata[album.id]?.startDate ?? undefined, - endDate: albumMetadata[album.id]?.endDate ?? undefined, + startDate: albumMetadata[album.id]?.startDate?.toISOString(), + endDate: albumMetadata[album.id]?.endDate?.toISOString(), assetCount: albumMetadata[album.id]?.assetCount ?? 0, // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need - lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined, + lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp?.toISOString(), })); } @@ -85,10 +85,10 @@ export class AlbumService extends BaseService { return { ...mapAlbum(album, withAssets, auth), - startDate: albumMetadataForIds?.startDate ?? undefined, - endDate: albumMetadataForIds?.endDate ?? undefined, + startDate: albumMetadataForIds?.startDate?.toISOString() ?? undefined, + endDate: albumMetadataForIds?.endDate?.toISOString() ?? undefined, assetCount: albumMetadataForIds?.assetCount ?? 0, - lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined, + lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp?.toISOString() ?? undefined, contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined, }; } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index a34efedfb0cb3..d37fa190f95d6 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -8,6 +8,7 @@ import { AuthService } from 'src/services/auth.service'; import { UserMetadataItem } from 'src/types'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userStub } from 'test/fixtures/user.stub'; import { factory, newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -231,16 +232,18 @@ describe(AuthService.name, () => { it('should sign up the admin', async () => { mocks.user.getAdmin.mockResolvedValue(void 0); mocks.user.create.mockResolvedValue({ + ...userStub.admin, ...dto, id: 'admin', + name: 'immich admin', createdAt: new Date('2021-01-01'), metadata: [] as UserMetadataItem[], - } as unknown as UserAdmin); + } as UserAdmin); await expect(sut.adminSignUp(dto)).resolves.toMatchObject({ avatarColor: expect.any(String), id: 'admin', - createdAt: new Date('2021-01-01'), + createdAt: new Date('2021-01-01').toISOString(), email: 'test@immich.com', name: 'immich admin', }); diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index 44929f2bbfd14..dc316d1938097 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -89,7 +89,7 @@ describe(MemoryService.name, () => { sut.create(factory.auth({ user: { id: userId } }), { type: memory.type, data: memory.data as OnThisDayData, - memoryAt: memory.memoryAt, + memoryAt: (memory.memoryAt as Date).toISOString(), isSaved: memory.isSaved, assetIds: [assetId], }), @@ -100,7 +100,7 @@ describe(MemoryService.name, () => { type: memory.type, data: memory.data, ownerId: memory.ownerId, - memoryAt: memory.memoryAt, + memoryAt: (memory.memoryAt as Date).toISOString(), isSaved: memory.isSaved, }, new Set(), @@ -120,7 +120,7 @@ describe(MemoryService.name, () => { type: memory.type, data: memory.data as OnThisDayData, assetIds: memory.assets.map((asset) => asset.id), - memoryAt: memory.memoryAt, + memoryAt: (memory.memoryAt as Date).toISOString(), }), ).resolves.toBeDefined(); @@ -139,7 +139,7 @@ describe(MemoryService.name, () => { sut.create(factory.auth(), { type: memory.type, data: memory.data as OnThisDayData, - memoryAt: memory.memoryAt, + memoryAt: (memory.memoryAt as Date).toISOString(), }), ).resolves.toBeDefined(); }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index d7c9fa9f59d40..d7b9c6663cade 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -23,7 +23,7 @@ const responseDto: PersonResponseDto = { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, - updatedAt: expect.any(Date), + updatedAt: new Date('2021-01-01').toISOString(), isFavorite: false, color: expect.any(String), }; @@ -62,7 +62,7 @@ describe(PersonService.name, () => { thumbnailPath: '/path/to/thumbnail.jpg', isHidden: true, isFavorite: false, - updatedAt: expect.any(Date), + updatedAt: new Date('2021-01-01').toISOString(), color: expect.any(String), }, ], @@ -91,7 +91,7 @@ describe(PersonService.name, () => { thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, isFavorite: true, - updatedAt: expect.any(Date), + updatedAt: new Date('2021-01-01').toISOString(), color: personStub.isFavorite.color, }, responseDto, @@ -203,7 +203,7 @@ describe(PersonService.name, () => { thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, isFavorite: false, - updatedAt: expect.any(Date), + updatedAt: new Date('2021-01-01').toISOString(), color: expect.any(String), }); expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); @@ -374,7 +374,7 @@ describe(PersonService.name, () => { id: personStub.noName.id, name: personStub.noName.name, thumbnailPath: personStub.noName.thumbnailPath, - updatedAt: expect.any(Date), + updatedAt: new Date('2021-01-01').toISOString(), color: personStub.noName.color, }, ); diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 7d2e99a215214..c14d13075b581 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -12,6 +12,7 @@ import { SchemaObject, } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import _ from 'lodash'; +import { cleanupOpenApiDoc } from 'nestjs-zod'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; import picomatch from 'picomatch'; @@ -149,6 +150,43 @@ function sortKeys(target: T): T { export const routeToErrorMessage = (methodName: string) => 'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`); +const stripSchemaMetadata = (schema: unknown) => { + if (!schema || typeof schema !== 'object') { + return schema; + } + + const clone = _.cloneDeep(schema) as Record; + delete clone.id; + return clone; +}; + +const replaceSchemaRefs = (target: unknown, schemaNameMap: Record) => { + if (!target || typeof target !== 'object') { + return; + } + + if (Array.isArray(target)) { + for (const item of target) { + replaceSchemaRefs(item, schemaNameMap); + } + return; + } + + const obj = target as Record; + const ref = obj.$ref; + if (typeof ref === 'string' && ref.startsWith('#/components/schemas/')) { + const name = ref.slice('#/components/schemas/'.length); + const mapped = schemaNameMap[name]; + if (mapped) { + obj.$ref = `#/components/schemas/${mapped}`; + } + } + + for (const value of Object.values(obj)) { + replaceSchemaRefs(value, schemaNameMap); + } +}; + const isSchema = (schema: string | ReferenceObject | SchemaObject): schema is SchemaObject => { if (typeof schema === 'string' || '$ref' in schema) { return false; @@ -162,10 +200,46 @@ const patchOpenAPI = (document: OpenAPIObject) => { if (document.components?.schemas) { const schemas = document.components.schemas as Record; + const schemaNameMap: Record = {}; + + /** + If X_Output exists and X does not exist → rename X_Output to X and rewrite all $refs. + If both exist and are deep-equal → rewrite refs to X, delete X_Output. + If both exist and differ → keep both (this is the "real" reason _Output exists). + */ + for (const outputName of Object.keys(schemas).filter((name) => name.endsWith('_Output'))) { + const baseName = outputName.slice(0, -'_Output'.length); + const outputSchema = schemas[outputName]; + const baseSchema = schemas[baseName]; + + if (!baseSchema) { + schemas[baseName] = outputSchema; + delete schemas[outputName]; + schemaNameMap[outputName] = baseName; + + const id = (outputSchema as Record).id; + if (id === outputName) { + (outputSchema as Record).id = baseName; + } + + continue; + } + + if (_.isEqual(stripSchemaMetadata(baseSchema), stripSchemaMetadata(outputSchema))) { + delete schemas[outputName]; + schemaNameMap[outputName] = baseName; + } + } + + replaceSchemaRefs(document, schemaNameMap); + + for (const schema of Object.values(schemas)) { + delete (schema as Record).id; + } document.components.schemas = sortKeys(schemas); - for (const [schemaName, schema] of Object.entries(schemas)) { + for (const [schemaName, schema] of Object.entries(document.components.schemas as Record)) { if (schema.properties) { schema.properties = sortKeys(schema.properties); @@ -265,6 +339,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) }; const specification = SwaggerModule.createDocument(app, config, options); + const openApiDoc = cleanupOpenApiDoc(specification); const customOptions: SwaggerCustomOptions = { swaggerOptions: { @@ -275,12 +350,12 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) customSiteTitle: 'Immich API Documentation', }; - SwaggerModule.setup('doc', app, specification, customOptions); + SwaggerModule.setup('doc', app, openApiDoc, customOptions); if (write) { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json'); - writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' }); + writeFileSync(outputPath, JSON.stringify(patchOpenAPI(openApiDoc), null, 2), { encoding: 'utf8' }); } }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index a42ff743bc16d..7c41c602ea66a 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -191,10 +191,10 @@ export const sharedLinkResponseStub = { allowDownload: true, allowUpload: true, assets: [], - createdAt: today, + createdAt: today.toISOString(), description: null, password: null, - expiresAt: tomorrow, + expiresAt: tomorrow.toISOString(), id: '123', key: sharedLinkBytes.toString('base64url'), showMetadata: true, @@ -207,10 +207,10 @@ export const sharedLinkResponseStub = { allowDownload: true, allowUpload: true, assets: [], - createdAt: today, + createdAt: today.toISOString(), description: null, password: null, - expiresAt: yesterday, + expiresAt: yesterday.toISOString(), id: '123', key: sharedLinkBytes.toString('base64url'), showMetadata: true, diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index ca66af7b94f93..40fe28a6702e9 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -55,15 +55,15 @@ export const tagStub = { export const tagResponseStub = { tag1: Object.freeze({ id: 'tag-1', - createdAt: new Date('2021-01-01T00:00:00Z'), - updatedAt: new Date('2021-01-01T00:00:00Z'), + createdAt: new Date('2021-01-01T00:00:00Z').toISOString(), + updatedAt: new Date('2021-01-01T00:00:00Z').toISOString(), name: 'Tag1', value: 'Tag1', }), color1: Object.freeze({ id: 'tag-1', - createdAt: new Date('2021-01-01T00:00:00Z'), - updatedAt: new Date('2021-01-01T00:00:00Z'), + createdAt: new Date('2021-01-01T00:00:00Z').toISOString(), + updatedAt: new Date('2021-01-01T00:00:00Z').toISOString(), color: '#000000', name: 'Tag1', value: 'Tag1', diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index e9f9daaf76dc2..dd6dd373ba3dc 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -47,6 +47,7 @@ export const errorDto = { error: 'Bad Request', statusCode: 400, message: message ?? expect.anything(), + correlationId: expect.any(String), }), noPermission: { error: 'Bad Request', diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index b3a3da6010312..e49fb5e88aa3b 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -47,15 +47,15 @@ describe(MemoryService.name, () => { const dto = { type: MemoryType.OnThisDay, data: { year: 2021 }, - memoryAt: new Date(2021), + memoryAt: new Date(2021).toISOString(), }; - await expect(sut.create(auth, dto)).resolves.toEqual({ + await expect(sut.create(auth, dto)).resolves.toMatchObject({ id: expect.any(String), type: dto.type, data: dto.data, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), + createdAt: expect.any(String), + updatedAt: expect.any(String), isSaved: false, memoryAt: dto.memoryAt, ownerId: user.id, @@ -72,7 +72,7 @@ describe(MemoryService.name, () => { const dto = { type: MemoryType.OnThisDay, data: { year: 2021 }, - memoryAt: new Date(2021), + memoryAt: new Date(2021).toISOString(), assetIds: [asset1.id, asset2.id], }; @@ -94,7 +94,7 @@ describe(MemoryService.name, () => { const dto = { type: MemoryType.OnThisDay, data: { year: 2021 }, - memoryAt: new Date(2021), + memoryAt: new Date(2021).toISOString(), assetIds: [asset1.id, asset2.id], }; diff --git a/server/test/medium/specs/services/shared-link.service.spec.ts b/server/test/medium/specs/services/shared-link.service.spec.ts index a43d0de9b93ee..2ada0e3e7b7ca 100644 --- a/server/test/medium/specs/services/shared-link.service.spec.ts +++ b/server/test/medium/specs/services/shared-link.service.spec.ts @@ -57,8 +57,8 @@ describe(SharedLinkService.name, () => { await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({ album: expect.objectContaining({ - startDate: '2020-01-01T00:00:00+00:00', - endDate: '2022-01-01T00:00:00+00:00', + startDate: '2020-01-01T00:00:00.000Z', + endDate: '2022-01-01T00:00:00.000Z', }), }); }); diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 4169c6e9bd009..62f23e26eb5c3 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -561,6 +561,7 @@ export const factory = { error: 'Bad Request', statusCode: 400, message: message ?? expect.anything(), + correlationId: expect.any(String), }), }, }; diff --git a/server/test/sql-tools/column-default-date.stub.ts b/server/test/sql-tools/column-default-date.stub.ts index 70f4d520f9c79..8f47658611ccc 100644 --- a/server/test/sql-tools/column-default-date.stub.ts +++ b/server/test/sql-tools/column-default-date.stub.ts @@ -1,6 +1,6 @@ import { Column, DatabaseSchema, Table } from 'src/sql-tools'; -const date = new Date(2023, 0, 1); +const date = new Date(Date.UTC(2023, 0, 1)); @Table() export class Table1 { @@ -29,7 +29,7 @@ export const schema: DatabaseSchema = { isArray: false, primary: false, synchronize: true, - default: "'2023-01-01T00:00:00.000Z'", + default: `'${date.toISOString()}'`, }, ], indexes: [], diff --git a/server/test/utils.ts b/server/test/utils.ts index c2a83c52aeb80..eb42a8a50a7a7 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,11 +1,12 @@ -import { CallHandler, ExecutionContext, Provider, ValidationPipe } from '@nestjs/common'; -import { APP_GUARD, APP_PIPE } from '@nestjs/core'; +import { CallHandler, ExecutionContext, Provider } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD, APP_PIPE } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; import { Test } from '@nestjs/testing'; import { ClassConstructor } from 'class-transformer'; import { NextFunction } from 'express'; import { Kysely } from 'kysely'; import multer from 'multer'; +import { ClsService } from 'nestjs-cls'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Duplex, Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; @@ -14,6 +15,8 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AuthGuard } from 'src/middleware/auth.guard'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; +import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; +import { HybridValidationPipe } from 'src/middleware/hybrid-validation.pipe'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -93,6 +96,7 @@ export type ControllerContext = { export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; const upload = multer({ storage: multer.memoryStorage() }); + const clsService = { getId: () => 'test-correlation-id' } satisfies Pick; const memoryFileInterceptor = { intercept: async (ctx: ExecutionContext, next: CallHandler) => { const context = ctx.switchToHttp(); @@ -113,8 +117,10 @@ export const controllerSetup = async (controller: ClassConstructor, pro const moduleRef = await Test.createTestingModule({ controllers: [controller], providers: [ - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_FILTER, useClass: GlobalExceptionFilter }, + { provide: APP_PIPE, useClass: HybridValidationPipe }, { provide: APP_GUARD, useClass: AuthGuard }, + { provide: ClsService, useValue: clsService }, { provide: LoggingRepository, useValue: LoggingRepository.create() }, { provide: AuthService, useValue: { authenticate: vi.fn() } }, ...providers, diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 73a6965dd92c4..84a31dec22536 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -331,7 +331,7 @@ export const deleteStack = async (stackIds: string[]) => { const assets = stacks.flatMap((stack) => stack.assets); for (const asset of assets) { - asset.stack = null; + asset.stack = undefined; } return assets; @@ -350,7 +350,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S toastManager.success($t('kept_this_deleted_others', { values: { count: assetsToDeleteIds.length } })); - keepAsset.stack = null; + keepAsset.stack = undefined; return keepAsset; } catch (error) { handleError(error, $t('errors.failed_to_keep_this_delete_others'));