mirror of
https://github.com/immich-app/immich.git
synced 2026-03-03 02:37:02 +00:00
Compare commits
11 Commits
d94d9600a7
...
proposal/z
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1c55883a8 | ||
|
|
864d320e3d | ||
|
|
832b0ecc8a | ||
|
|
3111685f3a | ||
|
|
d7cedb329e | ||
|
|
93841e1fea | ||
|
|
5621cf9fcd | ||
|
|
280b72f62c | ||
|
|
0ea71412f3 | ||
|
|
d86ac3e228 | ||
|
|
42f1cfd340 |
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -663,6 +663,7 @@ Class | Method | HTTP request | Description
|
||||
- [UserPreferencesResponseDto](doc//UserPreferencesResponseDto.md)
|
||||
- [UserPreferencesUpdateDto](doc//UserPreferencesUpdateDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [UserResponseDtoOutput](doc//UserResponseDtoOutput.md)
|
||||
- [UserStatus](doc//UserStatus.md)
|
||||
- [UserUpdateMeDto](doc//UserUpdateMeDto.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -402,6 +402,7 @@ 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_response_dto_output.dart';
|
||||
part 'model/user_status.dart';
|
||||
part 'model/user_update_me_dto.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
|
||||
4
mobile/openapi/lib/api/activities_api.dart
generated
4
mobile/openapi/lib/api/activities_api.dart
generated
@@ -136,10 +136,8 @@ class ActivitiesApi {
|
||||
/// Asset ID (if activity is for an asset)
|
||||
///
|
||||
/// * [ReactionLevel] level:
|
||||
/// Filter by activity level
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
/// Filter by activity type
|
||||
///
|
||||
/// * [String] userId:
|
||||
/// Filter by user ID
|
||||
@@ -195,10 +193,8 @@ class ActivitiesApi {
|
||||
/// Asset ID (if activity is for an asset)
|
||||
///
|
||||
/// * [ReactionLevel] level:
|
||||
/// Filter by activity level
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
/// Filter by activity type
|
||||
///
|
||||
/// * [String] userId:
|
||||
/// Filter by user ID
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -850,6 +850,8 @@ class ApiClient {
|
||||
return UserPreferencesUpdateDto.fromJson(value);
|
||||
case 'UserResponseDto':
|
||||
return UserResponseDto.fromJson(value);
|
||||
case 'UserResponseDtoOutput':
|
||||
return UserResponseDtoOutput.fromJson(value);
|
||||
case 'UserStatus':
|
||||
return UserStatusTypeTransformer().decode(value);
|
||||
case 'UserUpdateMeDto':
|
||||
|
||||
@@ -40,7 +40,6 @@ class ActivityCreateDto {
|
||||
///
|
||||
String? comment;
|
||||
|
||||
/// Activity type (like or comment)
|
||||
ReactionType type;
|
||||
|
||||
@override
|
||||
|
||||
11
mobile/openapi/lib/model/activity_response_dto.dart
generated
11
mobile/openapi/lib/model/activity_response_dto.dart
generated
@@ -33,10 +33,9 @@ class ActivityResponseDto {
|
||||
/// Activity ID
|
||||
String id;
|
||||
|
||||
/// Activity type
|
||||
ReactionType type;
|
||||
|
||||
UserResponseDto user;
|
||||
UserResponseDtoOutput user;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ActivityResponseDto &&
|
||||
@@ -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,10 +91,10 @@ class ActivityResponseDto {
|
||||
return ActivityResponseDto(
|
||||
assetId: mapValueOfType<String>(json, r'assetId'),
|
||||
comment: mapValueOfType<String>(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<String>(json, r'id')!,
|
||||
type: ReactionType.fromJson(json[r'type'])!,
|
||||
user: UserResponseDto.fromJson(json[r'user'])!,
|
||||
user: UserResponseDtoOutput.fromJson(json[r'user'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -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
|
||||
|
||||
2
mobile/openapi/lib/model/reaction_level.dart
generated
2
mobile/openapi/lib/model/reaction_level.dart
generated
@@ -10,7 +10,7 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
/// Reaction level
|
||||
class ReactionLevel {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReactionLevel._(this.value);
|
||||
|
||||
2
mobile/openapi/lib/model/reaction_type.dart
generated
2
mobile/openapi/lib/model/reaction_type.dart
generated
@@ -10,7 +10,7 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
/// Reaction type
|
||||
class ReactionType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReactionType._(this.value);
|
||||
|
||||
245
mobile/openapi/lib/model/user_response_dto_output.dart
generated
Normal file
245
mobile/openapi/lib/model/user_response_dto_output.dart
generated
Normal file
@@ -0,0 +1,245 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
class UserResponseDtoOutput {
|
||||
/// Returns a new [UserResponseDtoOutput] instance.
|
||||
UserResponseDtoOutput({
|
||||
required this.avatarColor,
|
||||
required this.email,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.profileChangedAt,
|
||||
required this.profileImagePath,
|
||||
});
|
||||
|
||||
/// Avatar color
|
||||
UserResponseDtoOutputAvatarColorEnum avatarColor;
|
||||
|
||||
/// User email
|
||||
String email;
|
||||
|
||||
/// User ID
|
||||
String id;
|
||||
|
||||
/// User name
|
||||
String name;
|
||||
|
||||
/// Profile change date
|
||||
DateTime profileChangedAt;
|
||||
|
||||
/// Profile image path
|
||||
String profileImagePath;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UserResponseDtoOutput &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
other.profileChangedAt == profileChangedAt &&
|
||||
other.profileImagePath == profileImagePath;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor.hashCode) +
|
||||
(email.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name.hashCode) +
|
||||
(profileChangedAt.hashCode) +
|
||||
(profileImagePath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserResponseDtoOutput[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'avatarColor'] = this.avatarColor;
|
||||
json[r'email'] = this.email;
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
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;
|
||||
}
|
||||
|
||||
/// Returns a new [UserResponseDtoOutput] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UserResponseDtoOutput? fromJson(dynamic value) {
|
||||
upgradeDto(value, "UserResponseDtoOutput");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UserResponseDtoOutput(
|
||||
avatarColor: UserResponseDtoOutputAvatarColorEnum.fromJson(json[r'avatarColor'])!,
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UserResponseDtoOutput> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UserResponseDtoOutput>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UserResponseDtoOutput.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UserResponseDtoOutput> mapFromJson(dynamic json) {
|
||||
final map = <String, UserResponseDtoOutput>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UserResponseDtoOutput.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UserResponseDtoOutput-objects as value to a dart map
|
||||
static Map<String, List<UserResponseDtoOutput>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UserResponseDtoOutput>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UserResponseDtoOutput.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'avatarColor',
|
||||
'email',
|
||||
'id',
|
||||
'name',
|
||||
'profileChangedAt',
|
||||
'profileImagePath',
|
||||
};
|
||||
}
|
||||
|
||||
/// Avatar color
|
||||
class UserResponseDtoOutputAvatarColorEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const UserResponseDtoOutputAvatarColorEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const primary = UserResponseDtoOutputAvatarColorEnum._(r'primary');
|
||||
static const pink = UserResponseDtoOutputAvatarColorEnum._(r'pink');
|
||||
static const red = UserResponseDtoOutputAvatarColorEnum._(r'red');
|
||||
static const yellow = UserResponseDtoOutputAvatarColorEnum._(r'yellow');
|
||||
static const blue = UserResponseDtoOutputAvatarColorEnum._(r'blue');
|
||||
static const green = UserResponseDtoOutputAvatarColorEnum._(r'green');
|
||||
static const purple = UserResponseDtoOutputAvatarColorEnum._(r'purple');
|
||||
static const orange = UserResponseDtoOutputAvatarColorEnum._(r'orange');
|
||||
static const gray = UserResponseDtoOutputAvatarColorEnum._(r'gray');
|
||||
static const amber = UserResponseDtoOutputAvatarColorEnum._(r'amber');
|
||||
|
||||
/// List of all possible values in this [enum][UserResponseDtoOutputAvatarColorEnum].
|
||||
static const values = <UserResponseDtoOutputAvatarColorEnum>[
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber,
|
||||
];
|
||||
|
||||
static UserResponseDtoOutputAvatarColorEnum? fromJson(dynamic value) => UserResponseDtoOutputAvatarColorEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<UserResponseDtoOutputAvatarColorEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UserResponseDtoOutputAvatarColorEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UserResponseDtoOutputAvatarColorEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [UserResponseDtoOutputAvatarColorEnum] to String,
|
||||
/// and [decode] dynamic data back to [UserResponseDtoOutputAvatarColorEnum].
|
||||
class UserResponseDtoOutputAvatarColorEnumTypeTransformer {
|
||||
factory UserResponseDtoOutputAvatarColorEnumTypeTransformer() => _instance ??= const UserResponseDtoOutputAvatarColorEnumTypeTransformer._();
|
||||
|
||||
const UserResponseDtoOutputAvatarColorEnumTypeTransformer._();
|
||||
|
||||
String encode(UserResponseDtoOutputAvatarColorEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a UserResponseDtoOutputAvatarColorEnum.
|
||||
///
|
||||
/// 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.
|
||||
UserResponseDtoOutputAvatarColorEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'primary': return UserResponseDtoOutputAvatarColorEnum.primary;
|
||||
case r'pink': return UserResponseDtoOutputAvatarColorEnum.pink;
|
||||
case r'red': return UserResponseDtoOutputAvatarColorEnum.red;
|
||||
case r'yellow': return UserResponseDtoOutputAvatarColorEnum.yellow;
|
||||
case r'blue': return UserResponseDtoOutputAvatarColorEnum.blue;
|
||||
case r'green': return UserResponseDtoOutputAvatarColorEnum.green;
|
||||
case r'purple': return UserResponseDtoOutputAvatarColorEnum.purple;
|
||||
case r'orange': return UserResponseDtoOutputAvatarColorEnum.orange;
|
||||
case r'gray': return UserResponseDtoOutputAvatarColorEnum.gray;
|
||||
case r'amber': return UserResponseDtoOutputAvatarColorEnum.amber;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [UserResponseDtoOutputAvatarColorEnumTypeTransformer] instance.
|
||||
static UserResponseDtoOutputAvatarColorEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
@@ -30,7 +32,6 @@
|
||||
"name": "level",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by activity level",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReactionLevel"
|
||||
}
|
||||
@@ -39,7 +40,6 @@
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by activity type",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
@@ -51,6 +51,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 +174,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 +185,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"
|
||||
}
|
||||
}
|
||||
@@ -15417,11 +15420,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 +15434,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
],
|
||||
"description": "Activity type (like or comment)"
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -15444,10 +15444,13 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityResponseDto": {
|
||||
"additionalProperties": false,
|
||||
"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,22 +15461,20 @@
|
||||
"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"
|
||||
"$ref": "#/components/schemas/UserResponseDto_Output"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -15486,13 +15487,18 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityStatisticsResponseDto": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"comments": {
|
||||
"description": "Number of comments",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"likes": {
|
||||
"description": "Number of likes",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -20789,6 +20795,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ReactionLevel": {
|
||||
"description": "Reaction level",
|
||||
"enum": [
|
||||
"album",
|
||||
"asset"
|
||||
@@ -20796,6 +20803,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReactionType": {
|
||||
"description": "Reaction type",
|
||||
"enum": [
|
||||
"comment",
|
||||
"like"
|
||||
@@ -25576,6 +25584,60 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserResponseDto_Output": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"description": "Avatar color",
|
||||
"enum": [
|
||||
"primary",
|
||||
"pink",
|
||||
"red",
|
||||
"yellow",
|
||||
"blue",
|
||||
"green",
|
||||
"purple",
|
||||
"orange",
|
||||
"gray",
|
||||
"amber"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"description": "User email",
|
||||
"type": "string"
|
||||
},
|
||||
"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": {
|
||||
"description": "User name",
|
||||
"type": "string"
|
||||
},
|
||||
"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": {
|
||||
"description": "Profile image path",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"email",
|
||||
"id",
|
||||
"name",
|
||||
"profileChangedAt",
|
||||
"profileImagePath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserStatus": {
|
||||
"description": "User status",
|
||||
"enum": [
|
||||
|
||||
@@ -14,9 +14,9 @@ const oazapfts = Oazapfts.runtime(defaults);
|
||||
export const servers = {
|
||||
server1: "/api"
|
||||
};
|
||||
export type UserResponseDto = {
|
||||
export type UserResponseDtoOutput = {
|
||||
/** Avatar color */
|
||||
avatarColor: UserAvatarColor;
|
||||
avatarColor: AvatarColor;
|
||||
/** User email */
|
||||
email: string;
|
||||
/** User ID */
|
||||
@@ -37,9 +37,8 @@ export type ActivityResponseDto = {
|
||||
createdAt: string;
|
||||
/** Activity ID */
|
||||
id: string;
|
||||
/** Activity type */
|
||||
"type": ReactionType;
|
||||
user: UserResponseDto;
|
||||
user: UserResponseDtoOutput;
|
||||
};
|
||||
export type ActivityCreateDto = {
|
||||
/** Album ID */
|
||||
@@ -48,7 +47,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 = {
|
||||
@@ -449,6 +447,20 @@ export type AssetStatsResponseDto = {
|
||||
/** Number of videos */
|
||||
videos: number;
|
||||
};
|
||||
export type UserResponseDto = {
|
||||
/** Avatar color */
|
||||
avatarColor: UserAvatarColor;
|
||||
/** User email */
|
||||
email: string;
|
||||
/** User ID */
|
||||
id: string;
|
||||
/** User name */
|
||||
name: string;
|
||||
/** Profile change date */
|
||||
profileChangedAt: string;
|
||||
/** Profile image path */
|
||||
profileImagePath: string;
|
||||
};
|
||||
export type AlbumUserResponseDto = {
|
||||
/** Album user role */
|
||||
role: AlbumUserRole;
|
||||
@@ -6782,7 +6794,7 @@ export enum ReactionType {
|
||||
Comment = "comment",
|
||||
Like = "like"
|
||||
}
|
||||
export enum UserAvatarColor {
|
||||
export enum AvatarColor {
|
||||
Primary = "primary",
|
||||
Pink = "pink",
|
||||
Red = "red",
|
||||
@@ -6822,6 +6834,18 @@ export enum NotificationType {
|
||||
AlbumUpdate = "AlbumUpdate",
|
||||
Custom = "Custom"
|
||||
}
|
||||
export enum UserAvatarColor {
|
||||
Primary = "primary",
|
||||
Pink = "pink",
|
||||
Red = "red",
|
||||
Yellow = "yellow",
|
||||
Blue = "blue",
|
||||
Green = "green",
|
||||
Purple = "purple",
|
||||
Orange = "orange",
|
||||
Gray = "gray",
|
||||
Amber = "amber"
|
||||
}
|
||||
export enum UserStatus {
|
||||
Active = "active",
|
||||
Removing = "removing",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -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
|
||||
@@ -9201,6 +9207,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==}
|
||||
|
||||
@@ -12283,6 +12300,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==}
|
||||
|
||||
@@ -22196,6 +22216,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:
|
||||
@@ -25925,6 +25954,8 @@ snapshots:
|
||||
|
||||
zod@4.2.1: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
zwitch@1.0.5: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, ZodValidationPipe } from 'nestjs-zod';
|
||||
import { commandsAndQuestions } from 'src/commands';
|
||||
import { IWorker } from 'src/constants';
|
||||
import { controllers } from 'src/controllers';
|
||||
@@ -41,7 +42,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: ZodValidationPipe },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
||||
];
|
||||
|
||||
@@ -73,4 +73,4 @@ export class ActivityController {
|
||||
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,111 @@
|
||||
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';
|
||||
|
||||
// Option 1 - reuse existing enum:
|
||||
export enum ReactionLevel {
|
||||
ALBUM = 'album',
|
||||
ASSET = 'asset',
|
||||
}
|
||||
export const ReactionLevelSchema = z.enum(ReactionLevel).describe('Reaction level').meta({ id: 'ReactionLevel' });
|
||||
|
||||
// Option 2 - define inline and access via .enum.XXX:
|
||||
export enum ReactionType {
|
||||
COMMENT = 'comment',
|
||||
LIKE = 'like',
|
||||
}
|
||||
export const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' });
|
||||
|
||||
export type MaybeDuplicate<T> = { 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;
|
||||
}
|
||||
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(),
|
||||
})
|
||||
.describe('Activity response');
|
||||
|
||||
export class ActivityStatisticsResponseDto {
|
||||
@ApiProperty({ type: 'integer', description: 'Number of comments' })
|
||||
comments!: number;
|
||||
const ActivityStatisticsResponseSchema = z
|
||||
.object({
|
||||
comments: z.number().int().min(0).describe('Number of comments'),
|
||||
likes: z.number().int().min(0).describe('Number of likes'),
|
||||
})
|
||||
.describe('Activity statistics response');
|
||||
|
||||
@ApiProperty({ type: 'integer', description: 'Number of likes' })
|
||||
likes!: number;
|
||||
}
|
||||
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');
|
||||
|
||||
export class ActivityDto {
|
||||
@ValidateUUID({ description: 'Album ID' })
|
||||
albumId!: string;
|
||||
const ActivitySearchSchema = ActivitySchema.extend({
|
||||
type: ReactionTypeSchema.optional(),
|
||||
level: ReactionLevelSchema.optional(),
|
||||
userId: z.uuidv4().describe('Filter by user ID').optional(),
|
||||
}).describe('Activity search');
|
||||
|
||||
@ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' })
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class ActivitySearchDto extends ActivityDto {
|
||||
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true })
|
||||
type?: ReactionType;
|
||||
|
||||
@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 isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT;
|
||||
|
||||
export class ActivityCreateDto extends ActivityDto {
|
||||
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' })
|
||||
type!: ReactionType;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Comment text (required if type is comment)' })
|
||||
@ValidateIf(isComment)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
}
|
||||
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 const mapActivity = (activity: Activity): ActivityResponseDto => {
|
||||
const type = activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT;
|
||||
|
||||
if (type === ReactionType.COMMENT) {
|
||||
return {
|
||||
id: activity.id,
|
||||
assetId: activity.assetId,
|
||||
createdAt: 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,
|
||||
};
|
||||
}
|
||||
|
||||
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 ActivityDto extends createZodDto(ActivitySchema) {}
|
||||
export class ActivitySearchDto extends createZodDto(ActivitySearchSchema) {}
|
||||
export class ActivityStatisticsResponseDto extends createZodDto(ActivityStatisticsResponseSchema) {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { User, UserAdmin } from 'src/database';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||
import { z } from 'zod';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
@ApiPropertyOptional({ description: 'User email' })
|
||||
@@ -51,6 +52,15 @@ export class UserResponseDto {
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
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: z.enum(UserAvatarColor).describe('Avatar color'),
|
||||
profileChangedAt: z.iso.datetime().describe('Profile change date'),
|
||||
}).meta({ id: 'UserResponseDto' });
|
||||
|
||||
export class UserLicense {
|
||||
@ApiProperty({ description: 'License key' })
|
||||
licenseKey!: string;
|
||||
|
||||
@@ -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<Error> {
|
||||
@@ -41,6 +43,20 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ describe(ActivityService.name, () => {
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await expect(sut.getAll(factory.auth(), { assetId, albumId, type: ReactionType.COMMENT })).resolves.toEqual([]);
|
||||
await expect(sut.getAll(factory.auth(), { assetId, albumId, type: ReactionType.COMMENT })).resolves.toEqual(
|
||||
[],
|
||||
);
|
||||
|
||||
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: false });
|
||||
});
|
||||
|
||||
@@ -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<T>(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<string, unknown>;
|
||||
delete clone.id;
|
||||
return clone;
|
||||
};
|
||||
|
||||
const replaceSchemaRefs = (target: unknown, schemaNameMap: Record<string, string>) => {
|
||||
if (!target || typeof target !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(target)) {
|
||||
for (const item of target) {
|
||||
replaceSchemaRefs(item, schemaNameMap);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = target as Record<string, unknown>;
|
||||
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<string, SchemaObject>;
|
||||
const schemaNameMap: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
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<string, unknown>).id;
|
||||
if (id === outputName) {
|
||||
(outputSchema as Record<string, unknown>).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<string, unknown>).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<string, SchemaObject>)) {
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user