mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	feat(server, web): quotas (#4471)
* feat: quotas * chore: open api * chore: update status box and upload error message --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									f4edb6c4bd
								
							
						
					
					
						commit
						deb1f970a8
					
				
							
								
								
									
										1
									
								
								mobile/openapi/doc/CreateUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/CreateUserDto.md
									
									
									
										generated
									
									
									
								
							@ -13,6 +13,7 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**memoriesEnabled** | **bool** |  | [optional] 
 | 
					**memoriesEnabled** | **bool** |  | [optional] 
 | 
				
			||||||
**name** | **String** |  | 
 | 
					**name** | **String** |  | 
 | 
				
			||||||
**password** | **String** |  | 
 | 
					**password** | **String** |  | 
 | 
				
			||||||
 | 
					**quotaSizeInBytes** | **int** |  | [optional] 
 | 
				
			||||||
**storageLabel** | **String** |  | [optional] 
 | 
					**storageLabel** | **String** |  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/PartnerResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/PartnerResponseDto.md
									
									
									
										generated
									
									
									
								
							@ -20,6 +20,8 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**name** | **String** |  | 
 | 
					**name** | **String** |  | 
 | 
				
			||||||
**oauthId** | **String** |  | 
 | 
					**oauthId** | **String** |  | 
 | 
				
			||||||
**profileImagePath** | **String** |  | 
 | 
					**profileImagePath** | **String** |  | 
 | 
				
			||||||
 | 
					**quotaSizeInBytes** | **int** |  | 
 | 
				
			||||||
 | 
					**quotaUsageInBytes** | **int** |  | 
 | 
				
			||||||
**shouldChangePassword** | **bool** |  | 
 | 
					**shouldChangePassword** | **bool** |  | 
 | 
				
			||||||
**storageLabel** | **String** |  | 
 | 
					**storageLabel** | **String** |  | 
 | 
				
			||||||
**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
					**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							@ -16,6 +16,7 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**memoriesEnabled** | **bool** |  | [optional] 
 | 
					**memoriesEnabled** | **bool** |  | [optional] 
 | 
				
			||||||
**name** | **String** |  | [optional] 
 | 
					**name** | **String** |  | [optional] 
 | 
				
			||||||
**password** | **String** |  | [optional] 
 | 
					**password** | **String** |  | [optional] 
 | 
				
			||||||
 | 
					**quotaSizeInBytes** | **int** |  | [optional] 
 | 
				
			||||||
**shouldChangePassword** | **bool** |  | [optional] 
 | 
					**shouldChangePassword** | **bool** |  | [optional] 
 | 
				
			||||||
**storageLabel** | **String** |  | [optional] 
 | 
					**storageLabel** | **String** |  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UsageByUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UsageByUserDto.md
									
									
									
										generated
									
									
									
								
							@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**photos** | **int** |  | 
 | 
					**photos** | **int** |  | 
 | 
				
			||||||
 | 
					**quotaSizeInBytes** | **int** |  | 
 | 
				
			||||||
**usage** | **int** |  | 
 | 
					**usage** | **int** |  | 
 | 
				
			||||||
**userId** | **String** |  | 
 | 
					**userId** | **String** |  | 
 | 
				
			||||||
**userName** | **String** |  | 
 | 
					**userName** | **String** |  | 
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/UserResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/UserResponseDto.md
									
									
									
										generated
									
									
									
								
							@ -19,6 +19,8 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**name** | **String** |  | 
 | 
					**name** | **String** |  | 
 | 
				
			||||||
**oauthId** | **String** |  | 
 | 
					**oauthId** | **String** |  | 
 | 
				
			||||||
**profileImagePath** | **String** |  | 
 | 
					**profileImagePath** | **String** |  | 
 | 
				
			||||||
 | 
					**quotaSizeInBytes** | **int** |  | 
 | 
				
			||||||
 | 
					**quotaUsageInBytes** | **int** |  | 
 | 
				
			||||||
**shouldChangePassword** | **bool** |  | 
 | 
					**shouldChangePassword** | **bool** |  | 
 | 
				
			||||||
**storageLabel** | **String** |  | 
 | 
					**storageLabel** | **String** |  | 
 | 
				
			||||||
**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
					**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								mobile/openapi/lib/model/create_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								mobile/openapi/lib/model/create_user_dto.dart
									
									
									
										generated
									
									
									
								
							@ -18,6 +18,7 @@ class CreateUserDto {
 | 
				
			|||||||
    this.memoriesEnabled,
 | 
					    this.memoriesEnabled,
 | 
				
			||||||
    required this.name,
 | 
					    required this.name,
 | 
				
			||||||
    required this.password,
 | 
					    required this.password,
 | 
				
			||||||
 | 
					    this.quotaSizeInBytes,
 | 
				
			||||||
    this.storageLabel,
 | 
					    this.storageLabel,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,6 +38,8 @@ class CreateUserDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String password;
 | 
					  String password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? storageLabel;
 | 
					  String? storageLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -46,6 +49,7 @@ class CreateUserDto {
 | 
				
			|||||||
     other.memoriesEnabled == memoriesEnabled &&
 | 
					     other.memoriesEnabled == memoriesEnabled &&
 | 
				
			||||||
     other.name == name &&
 | 
					     other.name == name &&
 | 
				
			||||||
     other.password == password &&
 | 
					     other.password == password &&
 | 
				
			||||||
 | 
					     other.quotaSizeInBytes == quotaSizeInBytes &&
 | 
				
			||||||
     other.storageLabel == storageLabel;
 | 
					     other.storageLabel == storageLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -56,10 +60,11 @@ class CreateUserDto {
 | 
				
			|||||||
    (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
 | 
					    (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
 | 
				
			||||||
    (name.hashCode) +
 | 
					    (name.hashCode) +
 | 
				
			||||||
    (password.hashCode) +
 | 
					    (password.hashCode) +
 | 
				
			||||||
 | 
					    (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
 | 
				
			||||||
    (storageLabel == null ? 0 : storageLabel!.hashCode);
 | 
					    (storageLabel == null ? 0 : storageLabel!.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, storageLabel=$storageLabel]';
 | 
					  String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -76,6 +81,11 @@ class CreateUserDto {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
      json[r'name'] = this.name;
 | 
					      json[r'name'] = this.name;
 | 
				
			||||||
      json[r'password'] = this.password;
 | 
					      json[r'password'] = this.password;
 | 
				
			||||||
 | 
					    if (this.quotaSizeInBytes != null) {
 | 
				
			||||||
 | 
					      json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					    //  json[r'quotaSizeInBytes'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (this.storageLabel != null) {
 | 
					    if (this.storageLabel != null) {
 | 
				
			||||||
      json[r'storageLabel'] = this.storageLabel;
 | 
					      json[r'storageLabel'] = this.storageLabel;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@ -97,6 +107,7 @@ class CreateUserDto {
 | 
				
			|||||||
        memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
 | 
					        memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
 | 
				
			||||||
        name: mapValueOfType<String>(json, r'name')!,
 | 
					        name: mapValueOfType<String>(json, r'name')!,
 | 
				
			||||||
        password: mapValueOfType<String>(json, r'password')!,
 | 
					        password: mapValueOfType<String>(json, r'password')!,
 | 
				
			||||||
 | 
					        quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
 | 
				
			||||||
        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
					        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								mobile/openapi/lib/model/partner_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								mobile/openapi/lib/model/partner_response_dto.dart
									
									
									
										generated
									
									
									
								
							@ -25,6 +25,8 @@ class PartnerResponseDto {
 | 
				
			|||||||
    required this.name,
 | 
					    required this.name,
 | 
				
			||||||
    required this.oauthId,
 | 
					    required this.oauthId,
 | 
				
			||||||
    required this.profileImagePath,
 | 
					    required this.profileImagePath,
 | 
				
			||||||
 | 
					    required this.quotaSizeInBytes,
 | 
				
			||||||
 | 
					    required this.quotaUsageInBytes,
 | 
				
			||||||
    required this.shouldChangePassword,
 | 
					    required this.shouldChangePassword,
 | 
				
			||||||
    required this.storageLabel,
 | 
					    required this.storageLabel,
 | 
				
			||||||
    required this.updatedAt,
 | 
					    required this.updatedAt,
 | 
				
			||||||
@ -66,6 +68,10 @@ class PartnerResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String profileImagePath;
 | 
					  String profileImagePath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int quotaUsageInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool shouldChangePassword;
 | 
					  bool shouldChangePassword;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? storageLabel;
 | 
					  String? storageLabel;
 | 
				
			||||||
@ -86,6 +92,8 @@ class PartnerResponseDto {
 | 
				
			|||||||
     other.name == name &&
 | 
					     other.name == name &&
 | 
				
			||||||
     other.oauthId == oauthId &&
 | 
					     other.oauthId == oauthId &&
 | 
				
			||||||
     other.profileImagePath == profileImagePath &&
 | 
					     other.profileImagePath == profileImagePath &&
 | 
				
			||||||
 | 
					     other.quotaSizeInBytes == quotaSizeInBytes &&
 | 
				
			||||||
 | 
					     other.quotaUsageInBytes == quotaUsageInBytes &&
 | 
				
			||||||
     other.shouldChangePassword == shouldChangePassword &&
 | 
					     other.shouldChangePassword == shouldChangePassword &&
 | 
				
			||||||
     other.storageLabel == storageLabel &&
 | 
					     other.storageLabel == storageLabel &&
 | 
				
			||||||
     other.updatedAt == updatedAt;
 | 
					     other.updatedAt == updatedAt;
 | 
				
			||||||
@ -105,12 +113,14 @@ class PartnerResponseDto {
 | 
				
			|||||||
    (name.hashCode) +
 | 
					    (name.hashCode) +
 | 
				
			||||||
    (oauthId.hashCode) +
 | 
					    (oauthId.hashCode) +
 | 
				
			||||||
    (profileImagePath.hashCode) +
 | 
					    (profileImagePath.hashCode) +
 | 
				
			||||||
 | 
					    (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
 | 
				
			||||||
 | 
					    (quotaUsageInBytes.hashCode) +
 | 
				
			||||||
    (shouldChangePassword.hashCode) +
 | 
					    (shouldChangePassword.hashCode) +
 | 
				
			||||||
    (storageLabel == null ? 0 : storageLabel!.hashCode) +
 | 
					    (storageLabel == null ? 0 : storageLabel!.hashCode) +
 | 
				
			||||||
    (updatedAt.hashCode);
 | 
					    (updatedAt.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
 | 
					  String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -142,6 +152,12 @@ class PartnerResponseDto {
 | 
				
			|||||||
      json[r'name'] = this.name;
 | 
					      json[r'name'] = this.name;
 | 
				
			||||||
      json[r'oauthId'] = this.oauthId;
 | 
					      json[r'oauthId'] = this.oauthId;
 | 
				
			||||||
      json[r'profileImagePath'] = this.profileImagePath;
 | 
					      json[r'profileImagePath'] = this.profileImagePath;
 | 
				
			||||||
 | 
					    if (this.quotaSizeInBytes != null) {
 | 
				
			||||||
 | 
					      json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					    //  json[r'quotaSizeInBytes'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					      json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
 | 
				
			||||||
      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
					      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
				
			||||||
    if (this.storageLabel != null) {
 | 
					    if (this.storageLabel != null) {
 | 
				
			||||||
      json[r'storageLabel'] = this.storageLabel;
 | 
					      json[r'storageLabel'] = this.storageLabel;
 | 
				
			||||||
@ -172,6 +188,8 @@ class PartnerResponseDto {
 | 
				
			|||||||
        name: mapValueOfType<String>(json, r'name')!,
 | 
					        name: mapValueOfType<String>(json, r'name')!,
 | 
				
			||||||
        oauthId: mapValueOfType<String>(json, r'oauthId')!,
 | 
					        oauthId: mapValueOfType<String>(json, r'oauthId')!,
 | 
				
			||||||
        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
					        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
				
			||||||
 | 
					        quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
 | 
				
			||||||
 | 
					        quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes')!,
 | 
				
			||||||
        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
					        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
				
			||||||
        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
					        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
				
			||||||
        updatedAt: mapDateTime(json, r'updatedAt', '')!,
 | 
					        updatedAt: mapDateTime(json, r'updatedAt', '')!,
 | 
				
			||||||
@ -232,6 +250,8 @@ class PartnerResponseDto {
 | 
				
			|||||||
    'name',
 | 
					    'name',
 | 
				
			||||||
    'oauthId',
 | 
					    'oauthId',
 | 
				
			||||||
    'profileImagePath',
 | 
					    'profileImagePath',
 | 
				
			||||||
 | 
					    'quotaSizeInBytes',
 | 
				
			||||||
 | 
					    'quotaUsageInBytes',
 | 
				
			||||||
    'shouldChangePassword',
 | 
					    'shouldChangePassword',
 | 
				
			||||||
    'storageLabel',
 | 
					    'storageLabel',
 | 
				
			||||||
    'updatedAt',
 | 
					    'updatedAt',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							@ -21,6 +21,7 @@ class UpdateUserDto {
 | 
				
			|||||||
    this.memoriesEnabled,
 | 
					    this.memoriesEnabled,
 | 
				
			||||||
    this.name,
 | 
					    this.name,
 | 
				
			||||||
    this.password,
 | 
					    this.password,
 | 
				
			||||||
 | 
					    this.quotaSizeInBytes,
 | 
				
			||||||
    this.shouldChangePassword,
 | 
					    this.shouldChangePassword,
 | 
				
			||||||
    this.storageLabel,
 | 
					    this.storageLabel,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -83,6 +84,8 @@ class UpdateUserDto {
 | 
				
			|||||||
  ///
 | 
					  ///
 | 
				
			||||||
  String? password;
 | 
					  String? password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// Please note: This property should have been non-nullable! Since the specification file
 | 
					  /// 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
 | 
					  /// does not include a default value (using the "default:" property), however, the generated
 | 
				
			||||||
@ -109,6 +112,7 @@ class UpdateUserDto {
 | 
				
			|||||||
     other.memoriesEnabled == memoriesEnabled &&
 | 
					     other.memoriesEnabled == memoriesEnabled &&
 | 
				
			||||||
     other.name == name &&
 | 
					     other.name == name &&
 | 
				
			||||||
     other.password == password &&
 | 
					     other.password == password &&
 | 
				
			||||||
 | 
					     other.quotaSizeInBytes == quotaSizeInBytes &&
 | 
				
			||||||
     other.shouldChangePassword == shouldChangePassword &&
 | 
					     other.shouldChangePassword == shouldChangePassword &&
 | 
				
			||||||
     other.storageLabel == storageLabel;
 | 
					     other.storageLabel == storageLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -123,11 +127,12 @@ class UpdateUserDto {
 | 
				
			|||||||
    (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
 | 
					    (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
 | 
				
			||||||
    (name == null ? 0 : name!.hashCode) +
 | 
					    (name == null ? 0 : name!.hashCode) +
 | 
				
			||||||
    (password == null ? 0 : password!.hashCode) +
 | 
					    (password == null ? 0 : password!.hashCode) +
 | 
				
			||||||
 | 
					    (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
 | 
				
			||||||
    (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
 | 
					    (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
 | 
				
			||||||
    (storageLabel == null ? 0 : storageLabel!.hashCode);
 | 
					    (storageLabel == null ? 0 : storageLabel!.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
 | 
					  String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -167,6 +172,11 @@ class UpdateUserDto {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
    //  json[r'password'] = null;
 | 
					    //  json[r'password'] = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.quotaSizeInBytes != null) {
 | 
				
			||||||
 | 
					      json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					    //  json[r'quotaSizeInBytes'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (this.shouldChangePassword != null) {
 | 
					    if (this.shouldChangePassword != null) {
 | 
				
			||||||
      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
					      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@ -196,6 +206,7 @@ class UpdateUserDto {
 | 
				
			|||||||
        memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
 | 
					        memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
 | 
				
			||||||
        name: mapValueOfType<String>(json, r'name'),
 | 
					        name: mapValueOfType<String>(json, r'name'),
 | 
				
			||||||
        password: mapValueOfType<String>(json, r'password'),
 | 
					        password: mapValueOfType<String>(json, r'password'),
 | 
				
			||||||
 | 
					        quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
 | 
				
			||||||
        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
 | 
					        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
 | 
				
			||||||
        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
					        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/usage_by_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/usage_by_user_dto.dart
									
									
									
										generated
									
									
									
								
							@ -14,6 +14,7 @@ class UsageByUserDto {
 | 
				
			|||||||
  /// Returns a new [UsageByUserDto] instance.
 | 
					  /// Returns a new [UsageByUserDto] instance.
 | 
				
			||||||
  UsageByUserDto({
 | 
					  UsageByUserDto({
 | 
				
			||||||
    required this.photos,
 | 
					    required this.photos,
 | 
				
			||||||
 | 
					    required this.quotaSizeInBytes,
 | 
				
			||||||
    required this.usage,
 | 
					    required this.usage,
 | 
				
			||||||
    required this.userId,
 | 
					    required this.userId,
 | 
				
			||||||
    required this.userName,
 | 
					    required this.userName,
 | 
				
			||||||
@ -22,6 +23,8 @@ class UsageByUserDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  int photos;
 | 
					  int photos;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int usage;
 | 
					  int usage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String userId;
 | 
					  String userId;
 | 
				
			||||||
@ -33,6 +36,7 @@ class UsageByUserDto {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto &&
 | 
				
			||||||
     other.photos == photos &&
 | 
					     other.photos == photos &&
 | 
				
			||||||
 | 
					     other.quotaSizeInBytes == quotaSizeInBytes &&
 | 
				
			||||||
     other.usage == usage &&
 | 
					     other.usage == usage &&
 | 
				
			||||||
     other.userId == userId &&
 | 
					     other.userId == userId &&
 | 
				
			||||||
     other.userName == userName &&
 | 
					     other.userName == userName &&
 | 
				
			||||||
@ -42,17 +46,23 @@ class UsageByUserDto {
 | 
				
			|||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (photos.hashCode) +
 | 
					    (photos.hashCode) +
 | 
				
			||||||
 | 
					    (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
 | 
				
			||||||
    (usage.hashCode) +
 | 
					    (usage.hashCode) +
 | 
				
			||||||
    (userId.hashCode) +
 | 
					    (userId.hashCode) +
 | 
				
			||||||
    (userName.hashCode) +
 | 
					    (userName.hashCode) +
 | 
				
			||||||
    (videos.hashCode);
 | 
					    (videos.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UsageByUserDto[photos=$photos, usage=$usage, userId=$userId, userName=$userName, videos=$videos]';
 | 
					  String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, userId=$userId, userName=$userName, videos=$videos]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
      json[r'photos'] = this.photos;
 | 
					      json[r'photos'] = this.photos;
 | 
				
			||||||
 | 
					    if (this.quotaSizeInBytes != null) {
 | 
				
			||||||
 | 
					      json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					    //  json[r'quotaSizeInBytes'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
      json[r'usage'] = this.usage;
 | 
					      json[r'usage'] = this.usage;
 | 
				
			||||||
      json[r'userId'] = this.userId;
 | 
					      json[r'userId'] = this.userId;
 | 
				
			||||||
      json[r'userName'] = this.userName;
 | 
					      json[r'userName'] = this.userName;
 | 
				
			||||||
@ -69,6 +79,7 @@ class UsageByUserDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return UsageByUserDto(
 | 
					      return UsageByUserDto(
 | 
				
			||||||
        photos: mapValueOfType<int>(json, r'photos')!,
 | 
					        photos: mapValueOfType<int>(json, r'photos')!,
 | 
				
			||||||
 | 
					        quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
 | 
				
			||||||
        usage: mapValueOfType<int>(json, r'usage')!,
 | 
					        usage: mapValueOfType<int>(json, r'usage')!,
 | 
				
			||||||
        userId: mapValueOfType<String>(json, r'userId')!,
 | 
					        userId: mapValueOfType<String>(json, r'userId')!,
 | 
				
			||||||
        userName: mapValueOfType<String>(json, r'userName')!,
 | 
					        userName: mapValueOfType<String>(json, r'userName')!,
 | 
				
			||||||
@ -121,6 +132,7 @@ class UsageByUserDto {
 | 
				
			|||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'photos',
 | 
					    'photos',
 | 
				
			||||||
 | 
					    'quotaSizeInBytes',
 | 
				
			||||||
    'usage',
 | 
					    'usage',
 | 
				
			||||||
    'userId',
 | 
					    'userId',
 | 
				
			||||||
    'userName',
 | 
					    'userName',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								mobile/openapi/lib/model/user_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								mobile/openapi/lib/model/user_response_dto.dart
									
									
									
										generated
									
									
									
								
							@ -24,6 +24,8 @@ class UserResponseDto {
 | 
				
			|||||||
    required this.name,
 | 
					    required this.name,
 | 
				
			||||||
    required this.oauthId,
 | 
					    required this.oauthId,
 | 
				
			||||||
    required this.profileImagePath,
 | 
					    required this.profileImagePath,
 | 
				
			||||||
 | 
					    required this.quotaSizeInBytes,
 | 
				
			||||||
 | 
					    required this.quotaUsageInBytes,
 | 
				
			||||||
    required this.shouldChangePassword,
 | 
					    required this.shouldChangePassword,
 | 
				
			||||||
    required this.storageLabel,
 | 
					    required this.storageLabel,
 | 
				
			||||||
    required this.updatedAt,
 | 
					    required this.updatedAt,
 | 
				
			||||||
@ -57,6 +59,10 @@ class UserResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String profileImagePath;
 | 
					  String profileImagePath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int quotaUsageInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool shouldChangePassword;
 | 
					  bool shouldChangePassword;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? storageLabel;
 | 
					  String? storageLabel;
 | 
				
			||||||
@ -76,6 +82,8 @@ class UserResponseDto {
 | 
				
			|||||||
     other.name == name &&
 | 
					     other.name == name &&
 | 
				
			||||||
     other.oauthId == oauthId &&
 | 
					     other.oauthId == oauthId &&
 | 
				
			||||||
     other.profileImagePath == profileImagePath &&
 | 
					     other.profileImagePath == profileImagePath &&
 | 
				
			||||||
 | 
					     other.quotaSizeInBytes == quotaSizeInBytes &&
 | 
				
			||||||
 | 
					     other.quotaUsageInBytes == quotaUsageInBytes &&
 | 
				
			||||||
     other.shouldChangePassword == shouldChangePassword &&
 | 
					     other.shouldChangePassword == shouldChangePassword &&
 | 
				
			||||||
     other.storageLabel == storageLabel &&
 | 
					     other.storageLabel == storageLabel &&
 | 
				
			||||||
     other.updatedAt == updatedAt;
 | 
					     other.updatedAt == updatedAt;
 | 
				
			||||||
@ -94,12 +102,14 @@ class UserResponseDto {
 | 
				
			|||||||
    (name.hashCode) +
 | 
					    (name.hashCode) +
 | 
				
			||||||
    (oauthId.hashCode) +
 | 
					    (oauthId.hashCode) +
 | 
				
			||||||
    (profileImagePath.hashCode) +
 | 
					    (profileImagePath.hashCode) +
 | 
				
			||||||
 | 
					    (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
 | 
				
			||||||
 | 
					    (quotaUsageInBytes.hashCode) +
 | 
				
			||||||
    (shouldChangePassword.hashCode) +
 | 
					    (shouldChangePassword.hashCode) +
 | 
				
			||||||
    (storageLabel == null ? 0 : storageLabel!.hashCode) +
 | 
					    (storageLabel == null ? 0 : storageLabel!.hashCode) +
 | 
				
			||||||
    (updatedAt.hashCode);
 | 
					    (updatedAt.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
 | 
					  String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -126,6 +136,12 @@ class UserResponseDto {
 | 
				
			|||||||
      json[r'name'] = this.name;
 | 
					      json[r'name'] = this.name;
 | 
				
			||||||
      json[r'oauthId'] = this.oauthId;
 | 
					      json[r'oauthId'] = this.oauthId;
 | 
				
			||||||
      json[r'profileImagePath'] = this.profileImagePath;
 | 
					      json[r'profileImagePath'] = this.profileImagePath;
 | 
				
			||||||
 | 
					    if (this.quotaSizeInBytes != null) {
 | 
				
			||||||
 | 
					      json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					    //  json[r'quotaSizeInBytes'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					      json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
 | 
				
			||||||
      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
					      json[r'shouldChangePassword'] = this.shouldChangePassword;
 | 
				
			||||||
    if (this.storageLabel != null) {
 | 
					    if (this.storageLabel != null) {
 | 
				
			||||||
      json[r'storageLabel'] = this.storageLabel;
 | 
					      json[r'storageLabel'] = this.storageLabel;
 | 
				
			||||||
@ -155,6 +171,8 @@ class UserResponseDto {
 | 
				
			|||||||
        name: mapValueOfType<String>(json, r'name')!,
 | 
					        name: mapValueOfType<String>(json, r'name')!,
 | 
				
			||||||
        oauthId: mapValueOfType<String>(json, r'oauthId')!,
 | 
					        oauthId: mapValueOfType<String>(json, r'oauthId')!,
 | 
				
			||||||
        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
					        profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
 | 
				
			||||||
 | 
					        quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
 | 
				
			||||||
 | 
					        quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes')!,
 | 
				
			||||||
        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
					        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
 | 
				
			||||||
        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
					        storageLabel: mapValueOfType<String>(json, r'storageLabel'),
 | 
				
			||||||
        updatedAt: mapDateTime(json, r'updatedAt', '')!,
 | 
					        updatedAt: mapDateTime(json, r'updatedAt', '')!,
 | 
				
			||||||
@ -215,6 +233,8 @@ class UserResponseDto {
 | 
				
			|||||||
    'name',
 | 
					    'name',
 | 
				
			||||||
    'oauthId',
 | 
					    'oauthId',
 | 
				
			||||||
    'profileImagePath',
 | 
					    'profileImagePath',
 | 
				
			||||||
 | 
					    'quotaSizeInBytes',
 | 
				
			||||||
 | 
					    'quotaUsageInBytes',
 | 
				
			||||||
    'shouldChangePassword',
 | 
					    'shouldChangePassword',
 | 
				
			||||||
    'storageLabel',
 | 
					    'storageLabel',
 | 
				
			||||||
    'updatedAt',
 | 
					    'updatedAt',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/create_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/create_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -41,6 +41,11 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaSizeInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaSizeInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // String storageLabel
 | 
					    // String storageLabel
 | 
				
			||||||
    test('to test the property `storageLabel`', () async {
 | 
					    test('to test the property `storageLabel`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								mobile/openapi/test/partner_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/test/partner_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -76,6 +76,16 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaSizeInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaSizeInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaUsageInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaUsageInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // bool shouldChangePassword
 | 
					    // bool shouldChangePassword
 | 
				
			||||||
    test('to test the property `shouldChangePassword`', () async {
 | 
					    test('to test the property `shouldChangePassword`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -56,6 +56,11 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaSizeInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaSizeInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // bool shouldChangePassword
 | 
					    // bool shouldChangePassword
 | 
				
			||||||
    test('to test the property `shouldChangePassword`', () async {
 | 
					    test('to test the property `shouldChangePassword`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/usage_by_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/usage_by_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -21,6 +21,11 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaSizeInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaSizeInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // int usage
 | 
					    // int usage
 | 
				
			||||||
    test('to test the property `usage`', () async {
 | 
					    test('to test the property `usage`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								mobile/openapi/test/user_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/test/user_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -71,6 +71,16 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaSizeInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaSizeInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int quotaUsageInBytes
 | 
				
			||||||
 | 
					    test('to test the property `quotaUsageInBytes`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // bool shouldChangePassword
 | 
					    // bool shouldChangePassword
 | 
				
			||||||
    test('to test the property `shouldChangePassword`', () async {
 | 
					    test('to test the property `shouldChangePassword`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
@ -7409,6 +7409,11 @@
 | 
				
			|||||||
          "password": {
 | 
					          "password": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaSizeInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "nullable": true,
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "storageLabel": {
 | 
					          "storageLabel": {
 | 
				
			||||||
            "nullable": true,
 | 
					            "nullable": true,
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
@ -8192,6 +8197,15 @@
 | 
				
			|||||||
          "profileImagePath": {
 | 
					          "profileImagePath": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaSizeInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "nullable": true,
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaUsageInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "shouldChangePassword": {
 | 
					          "shouldChangePassword": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -8206,10 +8220,8 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "avatarColor",
 | 
					          "avatarColor",
 | 
				
			||||||
          "id",
 | 
					          "quotaSizeInBytes",
 | 
				
			||||||
          "name",
 | 
					          "quotaUsageInBytes",
 | 
				
			||||||
          "email",
 | 
					 | 
				
			||||||
          "profileImagePath",
 | 
					 | 
				
			||||||
          "storageLabel",
 | 
					          "storageLabel",
 | 
				
			||||||
          "externalPath",
 | 
					          "externalPath",
 | 
				
			||||||
          "shouldChangePassword",
 | 
					          "shouldChangePassword",
 | 
				
			||||||
@ -8217,7 +8229,11 @@
 | 
				
			|||||||
          "createdAt",
 | 
					          "createdAt",
 | 
				
			||||||
          "deletedAt",
 | 
					          "deletedAt",
 | 
				
			||||||
          "updatedAt",
 | 
					          "updatedAt",
 | 
				
			||||||
          "oauthId"
 | 
					          "oauthId",
 | 
				
			||||||
 | 
					          "id",
 | 
				
			||||||
 | 
					          "name",
 | 
				
			||||||
 | 
					          "email",
 | 
				
			||||||
 | 
					          "profileImagePath"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -9757,6 +9773,11 @@
 | 
				
			|||||||
          "password": {
 | 
					          "password": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaSizeInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "nullable": true,
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "shouldChangePassword": {
 | 
					          "shouldChangePassword": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -9774,6 +9795,11 @@
 | 
				
			|||||||
          "photos": {
 | 
					          "photos": {
 | 
				
			||||||
            "type": "integer"
 | 
					            "type": "integer"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaSizeInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "nullable": true,
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "usage": {
 | 
					          "usage": {
 | 
				
			||||||
            "format": "int64",
 | 
					            "format": "int64",
 | 
				
			||||||
            "type": "integer"
 | 
					            "type": "integer"
 | 
				
			||||||
@ -9793,7 +9819,8 @@
 | 
				
			|||||||
          "userName",
 | 
					          "userName",
 | 
				
			||||||
          "photos",
 | 
					          "photos",
 | 
				
			||||||
          "videos",
 | 
					          "videos",
 | 
				
			||||||
          "usage"
 | 
					          "usage",
 | 
				
			||||||
 | 
					          "quotaSizeInBytes"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -9878,6 +9905,15 @@
 | 
				
			|||||||
          "profileImagePath": {
 | 
					          "profileImagePath": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaSizeInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "nullable": true,
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "quotaUsageInBytes": {
 | 
				
			||||||
 | 
					            "format": "int64",
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "shouldChangePassword": {
 | 
					          "shouldChangePassword": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -9892,10 +9928,8 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "avatarColor",
 | 
					          "avatarColor",
 | 
				
			||||||
          "id",
 | 
					          "quotaSizeInBytes",
 | 
				
			||||||
          "name",
 | 
					          "quotaUsageInBytes",
 | 
				
			||||||
          "email",
 | 
					 | 
				
			||||||
          "profileImagePath",
 | 
					 | 
				
			||||||
          "storageLabel",
 | 
					          "storageLabel",
 | 
				
			||||||
          "externalPath",
 | 
					          "externalPath",
 | 
				
			||||||
          "shouldChangePassword",
 | 
					          "shouldChangePassword",
 | 
				
			||||||
@ -9903,7 +9937,11 @@
 | 
				
			|||||||
          "createdAt",
 | 
					          "createdAt",
 | 
				
			||||||
          "deletedAt",
 | 
					          "deletedAt",
 | 
				
			||||||
          "updatedAt",
 | 
					          "updatedAt",
 | 
				
			||||||
          "oauthId"
 | 
					          "oauthId",
 | 
				
			||||||
 | 
					          "id",
 | 
				
			||||||
 | 
					          "name",
 | 
				
			||||||
 | 
					          "email",
 | 
				
			||||||
 | 
					          "profileImagePath"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										42
									
								
								open-api/typescript-sdk/client/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										42
									
								
								open-api/typescript-sdk/client/api.ts
									
									
									
										generated
									
									
									
								
							@ -1472,6 +1472,12 @@ export interface CreateUserDto {
 | 
				
			|||||||
     * @memberof CreateUserDto
 | 
					     * @memberof CreateUserDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'password': string;
 | 
					    'password': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof CreateUserDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaSizeInBytes'?: number | null;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
@ -2479,6 +2485,18 @@ export interface PartnerResponseDto {
 | 
				
			|||||||
     * @memberof PartnerResponseDto
 | 
					     * @memberof PartnerResponseDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'profileImagePath': string;
 | 
					    'profileImagePath': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof PartnerResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaSizeInBytes': number | null;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof PartnerResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaUsageInBytes': number;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
@ -4544,6 +4562,12 @@ export interface UpdateUserDto {
 | 
				
			|||||||
     * @memberof UpdateUserDto
 | 
					     * @memberof UpdateUserDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'password'?: string;
 | 
					    'password'?: string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof UpdateUserDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaSizeInBytes'?: number | null;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
@ -4571,6 +4595,12 @@ export interface UsageByUserDto {
 | 
				
			|||||||
     * @memberof UsageByUserDto
 | 
					     * @memberof UsageByUserDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'photos': number;
 | 
					    'photos': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof UsageByUserDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaSizeInBytes': number | null;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {number}
 | 
					     * @type {number}
 | 
				
			||||||
@ -4729,6 +4759,18 @@ export interface UserResponseDto {
 | 
				
			|||||||
     * @memberof UserResponseDto
 | 
					     * @memberof UserResponseDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'profileImagePath': string;
 | 
					    'profileImagePath': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof UserResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaSizeInBytes': number | null;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof UserResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'quotaUsageInBytes': number;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
				
			|||||||
@ -250,7 +250,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
 | 
				
			|||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual(user1Albums[0]);
 | 
					      expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should return album info for shared album', async () => {
 | 
					    it('should return album info for shared album', async () => {
 | 
				
			||||||
@ -259,7 +259,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
 | 
				
			|||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual(user2Albums[0]);
 | 
					      expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should return album info with assets when withoutAssets is undefined', async () => {
 | 
					    it('should return album info with assets when withoutAssets is undefined', async () => {
 | 
				
			||||||
@ -268,7 +268,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
 | 
				
			|||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual(user1Albums[0]);
 | 
					      expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should return album info without assets when withoutAssets is true', async () => {
 | 
					    it('should return album info without assets when withoutAssets is true', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ describe(`${AssetController.name} (e2e)`, () => {
 | 
				
			|||||||
  let assetRepository: IAssetRepository;
 | 
					  let assetRepository: IAssetRepository;
 | 
				
			||||||
  let user1: LoginResponseDto;
 | 
					  let user1: LoginResponseDto;
 | 
				
			||||||
  let user2: LoginResponseDto;
 | 
					  let user2: LoginResponseDto;
 | 
				
			||||||
 | 
					  let userWithQuota: LoginResponseDto;
 | 
				
			||||||
  let libraries: LibraryResponseDto[];
 | 
					  let libraries: LibraryResponseDto[];
 | 
				
			||||||
  let asset1: AssetResponseDto;
 | 
					  let asset1: AssetResponseDto;
 | 
				
			||||||
  let asset2: AssetResponseDto;
 | 
					  let asset2: AssetResponseDto;
 | 
				
			||||||
@ -75,11 +76,13 @@ describe(`${AssetController.name} (e2e)`, () => {
 | 
				
			|||||||
    await Promise.all([
 | 
					    await Promise.all([
 | 
				
			||||||
      api.userApi.create(server, admin.accessToken, userDto.user1),
 | 
					      api.userApi.create(server, admin.accessToken, userDto.user1),
 | 
				
			||||||
      api.userApi.create(server, admin.accessToken, userDto.user2),
 | 
					      api.userApi.create(server, admin.accessToken, userDto.user2),
 | 
				
			||||||
 | 
					      api.userApi.create(server, admin.accessToken, userDto.userWithQuota),
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [user1, user2] = await Promise.all([
 | 
					    [user1, user2, userWithQuota] = await Promise.all([
 | 
				
			||||||
      api.authApi.login(server, userDto.user1),
 | 
					      api.authApi.login(server, userDto.user1),
 | 
				
			||||||
      api.authApi.login(server, userDto.user2),
 | 
					      api.authApi.login(server, userDto.user2),
 | 
				
			||||||
 | 
					      api.authApi.login(server, userDto.userWithQuota),
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [user1Libraries, user2Libraries] = await Promise.all([
 | 
					    const [user1Libraries, user2Libraries] = await Promise.all([
 | 
				
			||||||
@ -634,6 +637,46 @@ describe(`${AssetController.name} (e2e)`, () => {
 | 
				
			|||||||
      expect(status).toBe(400);
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
      expect(body).toEqual(errorStub.badRequest('Not found or no asset.upload access'));
 | 
					      expect(body).toEqual(errorStub.badRequest('Not found or no asset.upload access'));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should update the used quota', async () => {
 | 
				
			||||||
 | 
					      const content = randomBytes(32);
 | 
				
			||||||
 | 
					      const { body, status } = await request(server)
 | 
				
			||||||
 | 
					        .post('/asset/upload')
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${userWithQuota.accessToken}`)
 | 
				
			||||||
 | 
					        .field('deviceAssetId', 'example-image')
 | 
				
			||||||
 | 
					        .field('deviceId', 'TEST')
 | 
				
			||||||
 | 
					        .field('fileCreatedAt', new Date().toISOString())
 | 
				
			||||||
 | 
					        .field('fileModifiedAt', new Date().toISOString())
 | 
				
			||||||
 | 
					        .field('isFavorite', 'true')
 | 
				
			||||||
 | 
					        .field('duration', '0:00:00.000000')
 | 
				
			||||||
 | 
					        .attach('assetData', content, 'example.jpg');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(status).toBe(201);
 | 
				
			||||||
 | 
					      expect(body).toEqual({ id: expect.any(String), duplicate: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { body: user } = await request(server)
 | 
				
			||||||
 | 
					        .get('/user/me')
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${userWithQuota.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 32 }));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should not upload an asset if it would exceed the quota', async () => {
 | 
				
			||||||
 | 
					      const content = randomBytes(420);
 | 
				
			||||||
 | 
					      const { body, status } = await request(server)
 | 
				
			||||||
 | 
					        .post('/asset/upload')
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${userWithQuota.accessToken}`)
 | 
				
			||||||
 | 
					        .field('deviceAssetId', 'example-image')
 | 
				
			||||||
 | 
					        .field('deviceId', 'TEST')
 | 
				
			||||||
 | 
					        .field('fileCreatedAt', new Date().toISOString())
 | 
				
			||||||
 | 
					        .field('fileModifiedAt', new Date().toISOString())
 | 
				
			||||||
 | 
					        .field('isFavorite', 'true')
 | 
				
			||||||
 | 
					        .field('duration', '0:00:00.000000')
 | 
				
			||||||
 | 
					        .attach('assetData', content, 'example.jpg');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorStub.badRequest('Quota has been exceeded!'));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('PUT /asset/:id', () => {
 | 
					  describe('PUT /asset/:id', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -32,6 +32,8 @@ const adminSignupResponse = {
 | 
				
			|||||||
  deletedAt: null,
 | 
					  deletedAt: null,
 | 
				
			||||||
  oauthId: '',
 | 
					  oauthId: '',
 | 
				
			||||||
  memoriesEnabled: true,
 | 
					  memoriesEnabled: true,
 | 
				
			||||||
 | 
					  quotaUsageInBytes: 0,
 | 
				
			||||||
 | 
					  quotaSizeInBytes: null,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(`${AuthController.name} (e2e)`, () => {
 | 
					describe(`${AuthController.name} (e2e)`, () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -128,6 +128,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
 | 
				
			|||||||
        usage: 0,
 | 
					        usage: 0,
 | 
				
			||||||
        usageByUser: [
 | 
					        usageByUser: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
 | 
					            quotaSizeInBytes: null,
 | 
				
			||||||
            photos: 0,
 | 
					            photos: 0,
 | 
				
			||||||
            usage: 0,
 | 
					            usage: 0,
 | 
				
			||||||
            userName: 'Immich Admin',
 | 
					            userName: 'Immich Admin',
 | 
				
			||||||
@ -135,6 +136,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
 | 
				
			|||||||
            videos: 0,
 | 
					            videos: 0,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
 | 
					            quotaSizeInBytes: null,
 | 
				
			||||||
            photos: 0,
 | 
					            photos: 0,
 | 
				
			||||||
            usage: 0,
 | 
					            usage: 0,
 | 
				
			||||||
            userName: 'User 1',
 | 
					            userName: 'User 1',
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import {
 | 
				
			|||||||
  newPartnerRepositoryMock,
 | 
					  newPartnerRepositoryMock,
 | 
				
			||||||
  newStorageRepositoryMock,
 | 
					  newStorageRepositoryMock,
 | 
				
			||||||
  newSystemConfigRepositoryMock,
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
 | 
					  newUserRepositoryMock,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
import { Readable } from 'stream';
 | 
					import { Readable } from 'stream';
 | 
				
			||||||
@ -28,6 +29,7 @@ import {
 | 
				
			|||||||
  IPartnerRepository,
 | 
					  IPartnerRepository,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
  ISystemConfigRepository,
 | 
					  ISystemConfigRepository,
 | 
				
			||||||
 | 
					  IUserRepository,
 | 
				
			||||||
  JobItem,
 | 
					  JobItem,
 | 
				
			||||||
  TimeBucketSize,
 | 
					  TimeBucketSize,
 | 
				
			||||||
} from '../repositories';
 | 
					} from '../repositories';
 | 
				
			||||||
@ -67,6 +69,7 @@ const uploadFile = {
 | 
				
			|||||||
      checksum: Buffer.from('checksum', 'utf8'),
 | 
					      checksum: Buffer.from('checksum', 'utf8'),
 | 
				
			||||||
      originalPath: 'upload/admin/image.jpeg',
 | 
					      originalPath: 'upload/admin/image.jpeg',
 | 
				
			||||||
      originalName: 'image.jpeg',
 | 
					      originalName: 'image.jpeg',
 | 
				
			||||||
 | 
					      size: 1000,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  filename: (fieldName: UploadFieldName, filename: string) => {
 | 
					  filename: (fieldName: UploadFieldName, filename: string) => {
 | 
				
			||||||
@ -79,6 +82,7 @@ const uploadFile = {
 | 
				
			|||||||
        checksum: Buffer.from('checksum', 'utf8'),
 | 
					        checksum: Buffer.from('checksum', 'utf8'),
 | 
				
			||||||
        originalPath: `upload/admin/${filename}`,
 | 
					        originalPath: `upload/admin/${filename}`,
 | 
				
			||||||
        originalName: filename,
 | 
					        originalName: filename,
 | 
				
			||||||
 | 
					        size: 1000,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -167,6 +171,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let storageMock: jest.Mocked<IStorageRepository>;
 | 
					  let storageMock: jest.Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					  let userMock: jest.Mocked<IUserRepository>;
 | 
				
			||||||
  let communicationMock: jest.Mocked<ICommunicationRepository>;
 | 
					  let communicationMock: jest.Mocked<ICommunicationRepository>;
 | 
				
			||||||
  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
					  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
				
			||||||
  let partnerMock: jest.Mocked<IPartnerRepository>;
 | 
					  let partnerMock: jest.Mocked<IPartnerRepository>;
 | 
				
			||||||
@ -182,6 +187,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    configMock = newSystemConfigRepositoryMock();
 | 
				
			||||||
    partnerMock = newPartnerRepositoryMock();
 | 
					    partnerMock = newPartnerRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -192,6 +198,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      configMock,
 | 
					      configMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
 | 
					      userMock,
 | 
				
			||||||
      communicationMock,
 | 
					      communicationMock,
 | 
				
			||||||
      partnerMock,
 | 
					      partnerMock,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -836,7 +843,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should remove faces', async () => {
 | 
					    it('should remove faces', async () => {
 | 
				
			||||||
      const assetWithFace = { ...(assetStub.image as AssetEntity), faces: [faceStub.face1, faceStub.mergeFace1] };
 | 
					      const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace);
 | 
					      when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -863,9 +870,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should update stack parent if asset has stack children', async () => {
 | 
					    it('should update stack parent if asset has stack children', async () => {
 | 
				
			||||||
      when(assetMock.getById)
 | 
					      when(assetMock.getById).calledWith(assetStub.primaryImage.id).mockResolvedValue(assetStub.primaryImage);
 | 
				
			||||||
        .calledWith(assetStub.primaryImage.id)
 | 
					 | 
				
			||||||
        .mockResolvedValue(assetStub.primaryImage as AssetEntity);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
 | 
					      await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -878,9 +883,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not schedule delete-files job for readonly assets', async () => {
 | 
					    it('should not schedule delete-files job for readonly assets', async () => {
 | 
				
			||||||
      when(assetMock.getById)
 | 
					      when(assetMock.getById).calledWith(assetStub.readOnly.id).mockResolvedValue(assetStub.readOnly);
 | 
				
			||||||
        .calledWith(assetStub.readOnly.id)
 | 
					 | 
				
			||||||
        .mockResolvedValue(assetStub.readOnly as AssetEntity);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleAssetDeletion({ id: assetStub.readOnly.id });
 | 
					      await sut.handleAssetDeletion({ id: assetStub.readOnly.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -890,21 +893,17 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not process assets from external library without fromExternal flag', async () => {
 | 
					    it('should not process assets from external library without fromExternal flag', async () => {
 | 
				
			||||||
      when(assetMock.getById)
 | 
					      when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external);
 | 
				
			||||||
        .calledWith(assetStub.external.id)
 | 
					 | 
				
			||||||
        .mockResolvedValue(assetStub.external as AssetEntity);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleAssetDeletion({ id: assetStub.external.id });
 | 
					      await sut.handleAssetDeletion({ id: assetStub.external.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(jobMock.queue).not.toBeCalled();
 | 
					      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.queueAll).not.toBeCalled();
 | 
					      expect(jobMock.queueAll).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(assetMock.remove).not.toBeCalled();
 | 
					      expect(assetMock.remove).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should process assets from external library with fromExternal flag', async () => {
 | 
					    it('should process assets from external library with fromExternal flag', async () => {
 | 
				
			||||||
      when(assetMock.getById)
 | 
					      when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external);
 | 
				
			||||||
        .calledWith(assetStub.external.id)
 | 
					 | 
				
			||||||
        .mockResolvedValue(assetStub.external as AssetEntity);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
 | 
					      await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -949,6 +948,13 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should update usage', async () => {
 | 
				
			||||||
 | 
					      when(assetMock.getById).calledWith(assetStub.image.id).mockResolvedValue(assetStub.image);
 | 
				
			||||||
 | 
					      await sut.handleAssetDeletion({ id: assetStub.image.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('run', () => {
 | 
					  describe('run', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@ import {
 | 
				
			|||||||
  IPartnerRepository,
 | 
					  IPartnerRepository,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
  ISystemConfigRepository,
 | 
					  ISystemConfigRepository,
 | 
				
			||||||
 | 
					  IUserRepository,
 | 
				
			||||||
  ImmichReadStream,
 | 
					  ImmichReadStream,
 | 
				
			||||||
  JobItem,
 | 
					  JobItem,
 | 
				
			||||||
  TimeBucketOptions,
 | 
					  TimeBucketOptions,
 | 
				
			||||||
@ -75,6 +76,7 @@ export interface UploadFile {
 | 
				
			|||||||
  checksum: Buffer;
 | 
					  checksum: Buffer;
 | 
				
			||||||
  originalPath: string;
 | 
					  originalPath: string;
 | 
				
			||||||
  originalName: string;
 | 
					  originalName: string;
 | 
				
			||||||
 | 
					  size: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AssetService {
 | 
					export class AssetService {
 | 
				
			||||||
@ -89,6 +91,7 @@ export class AssetService {
 | 
				
			|||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
 | 
					    @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
 | 
				
			||||||
    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
 | 
					    @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
@ -481,6 +484,7 @@ export class AssetService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.assetRepository.remove(asset);
 | 
					    await this.assetRepository.remove(asset);
 | 
				
			||||||
 | 
					    await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
 | 
				
			||||||
    this.communicationRepository.send(ClientEvent.ASSET_DELETE, asset.ownerId, id);
 | 
					    this.communicationRepository.send(ClientEvent.ASSET_DELETE, asset.ownerId, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO refactor this to use cascades
 | 
					    // TODO refactor this to use cascades
 | 
				
			||||||
 | 
				
			|||||||
@ -37,9 +37,10 @@ export enum JobName {
 | 
				
			|||||||
  METADATA_EXTRACTION = 'metadata-extraction',
 | 
					  METADATA_EXTRACTION = 'metadata-extraction',
 | 
				
			||||||
  LINK_LIVE_PHOTOS = 'link-live-photos',
 | 
					  LINK_LIVE_PHOTOS = 'link-live-photos',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // user deletion
 | 
					  // user
 | 
				
			||||||
  USER_DELETION = 'user-deletion',
 | 
					  USER_DELETION = 'user-deletion',
 | 
				
			||||||
  USER_DELETE_CHECK = 'user-delete-check',
 | 
					  USER_DELETE_CHECK = 'user-delete-check',
 | 
				
			||||||
 | 
					  USER_SYNC_USAGE = 'user-sync-usage',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // asset
 | 
					  // asset
 | 
				
			||||||
  ASSET_DELETION = 'asset-deletion',
 | 
					  ASSET_DELETION = 'asset-deletion',
 | 
				
			||||||
@ -95,6 +96,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
 | 
				
			|||||||
  [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
 | 
					  [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
 | 
				
			||||||
  [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
 | 
					  [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
 | 
				
			||||||
  [JobName.PERSON_DELETE]: QueueName.BACKGROUND_TASK,
 | 
					  [JobName.PERSON_DELETE]: QueueName.BACKGROUND_TASK,
 | 
				
			||||||
 | 
					  [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // conversion
 | 
					  // conversion
 | 
				
			||||||
  [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
 | 
					  [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
 | 
				
			||||||
 | 
				
			|||||||
@ -60,6 +60,7 @@ describe(JobService.name, () => {
 | 
				
			|||||||
        { name: JobName.PERSON_CLEANUP },
 | 
					        { name: JobName.PERSON_CLEANUP },
 | 
				
			||||||
        { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
 | 
					        { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
 | 
				
			||||||
        { name: JobName.CLEAN_OLD_AUDIT_LOGS },
 | 
					        { name: JobName.CLEAN_OLD_AUDIT_LOGS },
 | 
				
			||||||
 | 
					        { name: JobName.USER_SYNC_USAGE },
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -164,6 +164,7 @@ export class JobService {
 | 
				
			|||||||
      { name: JobName.PERSON_CLEANUP },
 | 
					      { name: JobName.PERSON_CLEANUP },
 | 
				
			||||||
      { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
 | 
					      { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
 | 
				
			||||||
      { name: JobName.CLEAN_OLD_AUDIT_LOGS },
 | 
					      { name: JobName.CLEAN_OLD_AUDIT_LOGS },
 | 
				
			||||||
 | 
					      { name: JobName.USER_SYNC_USAGE },
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,9 @@ const responseDto = {
 | 
				
			|||||||
    externalPath: null,
 | 
					    externalPath: null,
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
    inTimeline: true,
 | 
					    inTimeline: true,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  user1: <PartnerResponseDto>{
 | 
					  user1: <PartnerResponseDto>{
 | 
				
			||||||
    email: 'immich@test.com',
 | 
					    email: 'immich@test.com',
 | 
				
			||||||
@ -39,6 +41,8 @@ const responseDto = {
 | 
				
			|||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
    inTimeline: true,
 | 
					    inTimeline: true,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -39,9 +39,10 @@ export type JobItem =
 | 
				
			|||||||
  | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob }
 | 
					  | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob }
 | 
				
			||||||
  | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob }
 | 
					  | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // User Deletion
 | 
					  // User
 | 
				
			||||||
  | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob }
 | 
					  | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob }
 | 
				
			||||||
  | { name: JobName.USER_DELETION; data: IEntityJob }
 | 
					  | { name: JobName.USER_DELETION; data: IEntityJob }
 | 
				
			||||||
 | 
					  | { name: JobName.USER_SYNC_USAGE; data?: IBaseJob }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Storage Template
 | 
					  // Storage Template
 | 
				
			||||||
  | { name: JobName.STORAGE_TEMPLATE_MIGRATION; data?: IBaseJob }
 | 
					  | { name: JobName.STORAGE_TEMPLATE_MIGRATION; data?: IBaseJob }
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ export interface UserStatsQueryResponse {
 | 
				
			|||||||
  photos: number;
 | 
					  photos: number;
 | 
				
			||||||
  videos: number;
 | 
					  videos: number;
 | 
				
			||||||
  usage: number;
 | 
					  usage: number;
 | 
				
			||||||
 | 
					  quotaSizeInBytes: number | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface UserFindOptions {
 | 
					export interface UserFindOptions {
 | 
				
			||||||
@ -32,4 +33,6 @@ export interface IUserRepository {
 | 
				
			|||||||
  update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
 | 
					  update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
 | 
				
			||||||
  delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
 | 
					  delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
 | 
				
			||||||
  restore(user: UserEntity): Promise<UserEntity>;
 | 
					  restore(user: UserEntity): Promise<UserEntity>;
 | 
				
			||||||
 | 
					  updateUsage(id: string, delta: number): Promise<void>;
 | 
				
			||||||
 | 
					  syncUsage(): Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,8 @@ export class UsageByUserDto {
 | 
				
			|||||||
  videos!: number;
 | 
					  videos!: number;
 | 
				
			||||||
  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
  usage!: number;
 | 
					  usage!: number;
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
 | 
					  quotaSizeInBytes!: number | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ServerStatsResponseDto {
 | 
					export class ServerStatsResponseDto {
 | 
				
			||||||
 | 
				
			|||||||
@ -220,6 +220,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
          photos: 10,
 | 
					          photos: 10,
 | 
				
			||||||
          videos: 11,
 | 
					          videos: 11,
 | 
				
			||||||
          usage: 12345,
 | 
					          usage: 12345,
 | 
				
			||||||
 | 
					          quotaSizeInBytes: 0,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          userId: 'user2',
 | 
					          userId: 'user2',
 | 
				
			||||||
@ -227,6 +228,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
          photos: 10,
 | 
					          photos: 10,
 | 
				
			||||||
          videos: 20,
 | 
					          videos: 20,
 | 
				
			||||||
          usage: 123456,
 | 
					          usage: 123456,
 | 
				
			||||||
 | 
					          quotaSizeInBytes: 0,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          userId: 'user3',
 | 
					          userId: 'user3',
 | 
				
			||||||
@ -234,6 +236,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
          photos: 100,
 | 
					          photos: 100,
 | 
				
			||||||
          videos: 0,
 | 
					          videos: 0,
 | 
				
			||||||
          usage: 987654,
 | 
					          usage: 987654,
 | 
				
			||||||
 | 
					          quotaSizeInBytes: 0,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -244,6 +247,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
        usageByUser: [
 | 
					        usageByUser: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            photos: 10,
 | 
					            photos: 10,
 | 
				
			||||||
 | 
					            quotaSizeInBytes: 0,
 | 
				
			||||||
            usage: 12345,
 | 
					            usage: 12345,
 | 
				
			||||||
            userName: '1 User',
 | 
					            userName: '1 User',
 | 
				
			||||||
            userId: 'user1',
 | 
					            userId: 'user1',
 | 
				
			||||||
@ -251,6 +255,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            photos: 10,
 | 
					            photos: 10,
 | 
				
			||||||
 | 
					            quotaSizeInBytes: 0,
 | 
				
			||||||
            usage: 123456,
 | 
					            usage: 123456,
 | 
				
			||||||
            userName: '2 User',
 | 
					            userName: '2 User',
 | 
				
			||||||
            userId: 'user2',
 | 
					            userId: 'user2',
 | 
				
			||||||
@ -258,6 +263,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            photos: 100,
 | 
					            photos: 100,
 | 
				
			||||||
 | 
					            quotaSizeInBytes: 0,
 | 
				
			||||||
            usage: 987654,
 | 
					            usage: 987654,
 | 
				
			||||||
            userName: '3 User',
 | 
					            userName: '3 User',
 | 
				
			||||||
            userId: 'user3',
 | 
					            userId: 'user3',
 | 
				
			||||||
 | 
				
			|||||||
@ -118,6 +118,7 @@ export class ServerInfoService {
 | 
				
			|||||||
      usage.photos = user.photos;
 | 
					      usage.photos = user.photos;
 | 
				
			||||||
      usage.videos = user.videos;
 | 
					      usage.videos = user.videos;
 | 
				
			||||||
      usage.usage = user.usage;
 | 
					      usage.usage = user.usage;
 | 
				
			||||||
 | 
					      usage.quotaSizeInBytes = user.quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      serverStats.photos += usage.photos;
 | 
					      serverStats.photos += usage.photos;
 | 
				
			||||||
      serverStats.videos += usage.videos;
 | 
					      serverStats.videos += usage.videos;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
import { Transform } from 'class-transformer';
 | 
					import { Transform } from 'class-transformer';
 | 
				
			||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator';
 | 
					import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
 | 
				
			||||||
import { Optional, toEmail, toSanitized } from '../../domain.util';
 | 
					import { Optional, toEmail, toSanitized } from '../../domain.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CreateUserDto {
 | 
					export class CreateUserDto {
 | 
				
			||||||
@ -27,6 +28,12 @@ export class CreateUserDto {
 | 
				
			|||||||
  @Optional()
 | 
					  @Optional()
 | 
				
			||||||
  @IsBoolean()
 | 
					  @IsBoolean()
 | 
				
			||||||
  memoriesEnabled?: boolean;
 | 
					  memoriesEnabled?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional({ nullable: true })
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsPositive()
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
 | 
					  quotaSizeInBytes?: number | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CreateAdminDto {
 | 
					export class CreateAdminDto {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { UserAvatarColor } from '@app/infra/entities';
 | 
					import { UserAvatarColor } from '@app/infra/entities';
 | 
				
			||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
import { Transform } from 'class-transformer';
 | 
					import { Transform } from 'class-transformer';
 | 
				
			||||||
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator';
 | 
					import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
 | 
				
			||||||
import { Optional, toEmail, toSanitized } from '../../domain.util';
 | 
					import { Optional, toEmail, toSanitized } from '../../domain.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class UpdateUserDto {
 | 
					export class UpdateUserDto {
 | 
				
			||||||
@ -50,4 +50,10 @@ export class UpdateUserDto {
 | 
				
			|||||||
  @IsEnum(UserAvatarColor)
 | 
					  @IsEnum(UserAvatarColor)
 | 
				
			||||||
  @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
 | 
					  @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
 | 
				
			||||||
  avatarColor?: UserAvatarColor;
 | 
					  avatarColor?: UserAvatarColor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional({ nullable: true })
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsPositive()
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
 | 
					  quotaSizeInBytes?: number | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -33,6 +33,10 @@ export class UserResponseDto extends UserDto {
 | 
				
			|||||||
  updatedAt!: Date;
 | 
					  updatedAt!: Date;
 | 
				
			||||||
  oauthId!: string;
 | 
					  oauthId!: string;
 | 
				
			||||||
  memoriesEnabled?: boolean;
 | 
					  memoriesEnabled?: boolean;
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
 | 
					  quotaSizeInBytes!: number | null;
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer', format: 'int64' })
 | 
				
			||||||
 | 
					  quotaUsageInBytes!: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const mapSimpleUser = (entity: UserEntity): UserDto => {
 | 
					export const mapSimpleUser = (entity: UserEntity): UserDto => {
 | 
				
			||||||
@ -57,5 +61,7 @@ export function mapUser(entity: UserEntity): UserResponseDto {
 | 
				
			|||||||
    updatedAt: entity.updatedAt,
 | 
					    updatedAt: entity.updatedAt,
 | 
				
			||||||
    oauthId: entity.oauthId,
 | 
					    oauthId: entity.oauthId,
 | 
				
			||||||
    memoriesEnabled: entity.memoriesEnabled,
 | 
					    memoriesEnabled: entity.memoriesEnabled,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: entity.quotaSizeInBytes,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: entity.quotaUsageInBytes,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -512,4 +512,11 @@ describe(UserService.name, () => {
 | 
				
			|||||||
      expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options);
 | 
					      expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleUserSyncUsage', () => {
 | 
				
			||||||
 | 
					    it('should sync usage', async () => {
 | 
				
			||||||
 | 
					      await sut.handleUserSyncUsage();
 | 
				
			||||||
 | 
					      expect(userMock.syncUsage).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -127,6 +127,11 @@ export class UserService {
 | 
				
			|||||||
    return { admin, password, provided: !!providedPassword };
 | 
					    return { admin, password, provided: !!providedPassword };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleUserSyncUsage() {
 | 
				
			||||||
 | 
					    await this.userRepository.syncUsage();
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleUserDeleteCheck() {
 | 
					  async handleUserDeleteCheck() {
 | 
				
			||||||
    const users = await this.userRepository.getDeletedUsers();
 | 
					    const users = await this.userRepository.getDeletedUsers();
 | 
				
			||||||
    await this.jobRepository.queueAll(
 | 
					    await this.jobRepository.queueAll(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { AssetCreate } from '@app/domain';
 | 
					import { AssetCreate } from '@app/domain';
 | 
				
			||||||
import { AssetEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { OptionalBetween } from '@app/infra/infra.utils';
 | 
					import { OptionalBetween } from '@app/infra/infra.utils';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
@ -23,6 +23,7 @@ export interface AssetOwnerCheck extends AssetCheck {
 | 
				
			|||||||
export interface IAssetRepository {
 | 
					export interface IAssetRepository {
 | 
				
			||||||
  get(id: string): Promise<AssetEntity | null>;
 | 
					  get(id: string): Promise<AssetEntity | null>;
 | 
				
			||||||
  create(asset: AssetCreate): Promise<AssetEntity>;
 | 
					  create(asset: AssetCreate): Promise<AssetEntity>;
 | 
				
			||||||
 | 
					  upsertExif(exif: Partial<ExifEntity>): Promise<void>;
 | 
				
			||||||
  getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
 | 
					  getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
 | 
				
			||||||
  getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
 | 
					  getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
 | 
				
			||||||
  getById(assetId: string): Promise<AssetEntity>;
 | 
					  getById(assetId: string): Promise<AssetEntity>;
 | 
				
			||||||
@ -38,7 +39,10 @@ export const IAssetRepository = 'IAssetRepository';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetRepository implements IAssetRepository {
 | 
					export class AssetRepository implements IAssetRepository {
 | 
				
			||||||
  constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
 | 
					  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
 | 
				
			||||||
    return this.assetRepository
 | 
					    return this.assetRepository
 | 
				
			||||||
@ -162,6 +166,10 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
    return this.assetRepository.save(asset);
 | 
					    return this.assetRepository.save(asset);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsertExif(exif: Partial<ExifEntity>): Promise<void> {
 | 
				
			||||||
 | 
					    await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get assets by device's Id on the database
 | 
					   * Get assets by device's Id on the database
 | 
				
			||||||
   * @param ownerId
 | 
					   * @param ownerId
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain';
 | 
					import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain';
 | 
				
			||||||
import { AssetEntity } from '@app/infra/entities';
 | 
					import { AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
import { parse } from 'node:path';
 | 
					import { parse } from 'node:path';
 | 
				
			||||||
import { IAssetRepository } from './asset-repository';
 | 
					import { IAssetRepository } from './asset-repository';
 | 
				
			||||||
import { CreateAssetDto } from './dto/create-asset.dto';
 | 
					import { CreateAssetDto } from './dto/create-asset.dto';
 | 
				
			||||||
@ -52,8 +53,15 @@ export class AssetCore {
 | 
				
			|||||||
      isOffline: dto.isOffline ?? false,
 | 
					      isOffline: dto.isOffline ?? false,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.repository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
 | 
				
			||||||
    await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
 | 
					    await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return asset;
 | 
					    return asset;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static requireQuota(auth: AuthDto, size: number) {
 | 
				
			||||||
 | 
					    if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Quota has been exceeded!');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { IJobRepository, ILibraryRepository, JobName } from '@app/domain';
 | 
					import { IJobRepository, ILibraryRepository, IUserRepository, JobName } from '@app/domain';
 | 
				
			||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
					import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException } from '@nestjs/common';
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
  newAccessRepositoryMock,
 | 
					  newAccessRepositoryMock,
 | 
				
			||||||
  newJobRepositoryMock,
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
  newLibraryRepositoryMock,
 | 
					  newLibraryRepositoryMock,
 | 
				
			||||||
 | 
					  newUserRepositoryMock,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
import { QueryFailedError } from 'typeorm';
 | 
					import { QueryFailedError } from 'typeorm';
 | 
				
			||||||
@ -87,11 +88,13 @@ describe('AssetService', () => {
 | 
				
			|||||||
  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let libraryMock: jest.Mocked<ILibraryRepository>;
 | 
					  let libraryMock: jest.Mocked<ILibraryRepository>;
 | 
				
			||||||
 | 
					  let userMock: jest.Mocked<IUserRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetRepositoryMock = {
 | 
					    assetRepositoryMock = {
 | 
				
			||||||
      get: jest.fn(),
 | 
					      get: jest.fn(),
 | 
				
			||||||
      create: jest.fn(),
 | 
					      create: jest.fn(),
 | 
				
			||||||
 | 
					      upsertExif: jest.fn(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      getAllByUserId: jest.fn(),
 | 
					      getAllByUserId: jest.fn(),
 | 
				
			||||||
      getAllByDeviceId: jest.fn(),
 | 
					      getAllByDeviceId: jest.fn(),
 | 
				
			||||||
@ -107,8 +110,9 @@ describe('AssetService', () => {
 | 
				
			|||||||
    accessMock = newAccessRepositoryMock();
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    libraryMock = newLibraryRepositoryMock();
 | 
					    libraryMock = newLibraryRepositoryMock();
 | 
				
			||||||
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock);
 | 
					    sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock, userMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(assetRepositoryMock.get)
 | 
					    when(assetRepositoryMock.get)
 | 
				
			||||||
      .calledWith(assetStub.livePhotoStillAsset.id)
 | 
					      .calledWith(assetStub.livePhotoStillAsset.id)
 | 
				
			||||||
@ -127,6 +131,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
        mimeType: 'image/jpeg',
 | 
					        mimeType: 'image/jpeg',
 | 
				
			||||||
        checksum: Buffer.from('file hash', 'utf8'),
 | 
					        checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
        originalName: 'asset_1.jpeg',
 | 
					        originalName: 'asset_1.jpeg',
 | 
				
			||||||
 | 
					        size: 42,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      const dto = _getCreateAssetDto();
 | 
					      const dto = _getCreateAssetDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -136,6 +141,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
      await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
 | 
					      await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetRepositoryMock.create).toHaveBeenCalled();
 | 
					      expect(assetRepositoryMock.create).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should handle a duplicate', async () => {
 | 
					    it('should handle a duplicate', async () => {
 | 
				
			||||||
@ -145,6 +151,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
        mimeType: 'image/jpeg',
 | 
					        mimeType: 'image/jpeg',
 | 
				
			||||||
        checksum: Buffer.from('file hash', 'utf8'),
 | 
					        checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
        originalName: 'asset_1.jpeg',
 | 
					        originalName: 'asset_1.jpeg',
 | 
				
			||||||
 | 
					        size: 0,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      const dto = _getCreateAssetDto();
 | 
					      const dto = _getCreateAssetDto();
 | 
				
			||||||
      const error = new QueryFailedError('', [], '');
 | 
					      const error = new QueryFailedError('', [], '');
 | 
				
			||||||
@ -160,6 +167,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
        name: JobName.DELETE_FILES,
 | 
					        name: JobName.DELETE_FILES,
 | 
				
			||||||
        data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
 | 
					        data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      expect(userMock.updateUsage).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should handle a live photo', async () => {
 | 
					    it('should handle a live photo', async () => {
 | 
				
			||||||
@ -187,6 +195,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        [{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
 | 
					        [{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					      expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  IJobRepository,
 | 
					  IJobRepository,
 | 
				
			||||||
  ILibraryRepository,
 | 
					  ILibraryRepository,
 | 
				
			||||||
  ImmichFileResponse,
 | 
					  ImmichFileResponse,
 | 
				
			||||||
 | 
					  IUserRepository,
 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
  mapAsset,
 | 
					  mapAsset,
 | 
				
			||||||
  mimeTypes,
 | 
					  mimeTypes,
 | 
				
			||||||
@ -49,6 +50,7 @@ export class AssetService {
 | 
				
			|||||||
    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
 | 
					    @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
 | 
				
			||||||
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.assetCore = new AssetCore(_assetRepository, jobRepository);
 | 
					    this.assetCore = new AssetCore(_assetRepository, jobRepository);
 | 
				
			||||||
    this.access = AccessCore.create(accessRepository);
 | 
					    this.access = AccessCore.create(accessRepository);
 | 
				
			||||||
@ -73,6 +75,7 @@ export class AssetService {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const libraryId = await this.getLibraryId(auth, dto.libraryId);
 | 
					      const libraryId = await this.getLibraryId(auth, dto.libraryId);
 | 
				
			||||||
      await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
 | 
					      await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
 | 
				
			||||||
 | 
					      AssetCore.requireQuota(auth, file.size);
 | 
				
			||||||
      if (livePhotoFile) {
 | 
					      if (livePhotoFile) {
 | 
				
			||||||
        const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
 | 
					        const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
 | 
				
			||||||
        livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile);
 | 
					        livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile);
 | 
				
			||||||
@ -86,6 +89,8 @@ export class AssetService {
 | 
				
			|||||||
        sidecarFile?.originalPath,
 | 
					        sidecarFile?.originalPath,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return { id: asset.id, duplicate: false };
 | 
					      return { id: asset.id, duplicate: false };
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      // clean up files
 | 
					      // clean up files
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { DomainModule } from '@app/domain';
 | 
					import { DomainModule } from '@app/domain';
 | 
				
			||||||
import { InfraModule } from '@app/infra';
 | 
					import { InfraModule } from '@app/infra';
 | 
				
			||||||
import { AssetEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Module, OnModuleInit } from '@nestjs/common';
 | 
					import { Module, OnModuleInit } from '@nestjs/common';
 | 
				
			||||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
 | 
					import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
 | 
				
			||||||
import { ScheduleModule } from '@nestjs/schedule';
 | 
					import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			||||||
@ -40,7 +40,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
 | 
				
			|||||||
    InfraModule,
 | 
					    InfraModule,
 | 
				
			||||||
    DomainModule,
 | 
					    DomainModule,
 | 
				
			||||||
    ScheduleModule.forRoot(),
 | 
					    ScheduleModule.forRoot(),
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity]),
 | 
					    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    ActivityController,
 | 
					    ActivityController,
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@ export function mapToUploadFile(file: ImmichFile): UploadFile {
 | 
				
			|||||||
    checksum: file.checksum,
 | 
					    checksum: file.checksum,
 | 
				
			||||||
    originalPath: file.path,
 | 
					    originalPath: file.path,
 | 
				
			||||||
    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
 | 
					    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
 | 
				
			||||||
 | 
					    size: file.size,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -75,4 +75,10 @@ export class UserEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @OneToMany(() => AssetEntity, (asset) => asset.owner)
 | 
					  @OneToMany(() => AssetEntity, (asset) => asset.owner)
 | 
				
			||||||
  assets!: AssetEntity[];
 | 
					  assets!: AssetEntity[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'bigint', nullable: true })
 | 
				
			||||||
 | 
					  quotaSizeInBytes!: number | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'bigint', default: 0 })
 | 
				
			||||||
 | 
					  quotaUsageInBytes!: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from "typeorm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AddQuotaColumnsToUser1704382918223 implements MigrationInterface {
 | 
				
			||||||
 | 
					    name = 'AddQuotaColumnsToUser1704382918223'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "users" ADD "quotaSizeInBytes" bigint`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "users" ADD "quotaUsageInBytes" bigint NOT NULL DEFAULT '0'`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async down(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "quotaUsageInBytes"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "quotaSizeInBytes"`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,12 +2,15 @@ import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryRespons
 | 
				
			|||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { IsNull, Not, Repository } from 'typeorm';
 | 
					import { IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
import { UserEntity } from '../entities';
 | 
					import { AssetEntity, UserEntity } from '../entities';
 | 
				
			||||||
import { DummyValue, GenerateSql } from '../infra.util';
 | 
					import { DummyValue, GenerateSql } from '../infra.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class UserRepository implements IUserRepository {
 | 
					export class UserRepository implements IUserRepository {
 | 
				
			||||||
  constructor(@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>) {}
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					    @InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async get(userId: string, options: UserFindOptions): Promise<UserEntity | null> {
 | 
					  async get(userId: string, options: UserFindOptions): Promise<UserEntity | null> {
 | 
				
			||||||
    options = options || {};
 | 
					    options = options || {};
 | 
				
			||||||
@ -91,6 +94,7 @@ export class UserRepository implements IUserRepository {
 | 
				
			|||||||
      .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
 | 
					      .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
 | 
				
			||||||
      .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
 | 
					      .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
 | 
				
			||||||
      .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
 | 
					      .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
 | 
				
			||||||
 | 
					      .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
 | 
				
			||||||
      .leftJoin('users.assets', 'assets')
 | 
					      .leftJoin('users.assets', 'assets')
 | 
				
			||||||
      .leftJoin('assets.exifInfo', 'exif')
 | 
					      .leftJoin('assets.exifInfo', 'exif')
 | 
				
			||||||
      .groupBy('users.id')
 | 
					      .groupBy('users.id')
 | 
				
			||||||
@ -101,11 +105,32 @@ export class UserRepository implements IUserRepository {
 | 
				
			|||||||
      stat.photos = Number(stat.photos);
 | 
					      stat.photos = Number(stat.photos);
 | 
				
			||||||
      stat.videos = Number(stat.videos);
 | 
					      stat.videos = Number(stat.videos);
 | 
				
			||||||
      stat.usage = Number(stat.usage);
 | 
					      stat.usage = Number(stat.usage);
 | 
				
			||||||
 | 
					      stat.quotaSizeInBytes = stat.quotaSizeInBytes;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return stats;
 | 
					    return stats;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async updateUsage(id: string, delta: number): Promise<void> {
 | 
				
			||||||
 | 
					    await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async syncUsage() {
 | 
				
			||||||
 | 
					    const subQuery = this.assetRepository
 | 
				
			||||||
 | 
					      .createQueryBuilder('assets')
 | 
				
			||||||
 | 
					      .select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
 | 
				
			||||||
 | 
					      .leftJoin('assets.exifInfo', 'exif')
 | 
				
			||||||
 | 
					      .where('assets.ownerId = users.id')
 | 
				
			||||||
 | 
					      .withDeleted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.userRepository
 | 
				
			||||||
 | 
					      .createQueryBuilder('users')
 | 
				
			||||||
 | 
					      .leftJoin('users.assets', 'assets')
 | 
				
			||||||
 | 
					      .update()
 | 
				
			||||||
 | 
					      .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` })
 | 
				
			||||||
 | 
					      .execute();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async save(user: Partial<UserEntity>) {
 | 
					  private async save(user: Partial<UserEntity>) {
 | 
				
			||||||
    const { id } = await this.userRepository.save(user);
 | 
					    const { id } = await this.userRepository.save(user);
 | 
				
			||||||
    return this.userRepository.findOneByOrFail({ id });
 | 
					    return this.userRepository.findOneByOrFail({ id });
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,8 @@ FROM
 | 
				
			|||||||
      "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					      "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					      "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					      "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					      "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
				
			||||||
@ -43,6 +45,8 @@ FROM
 | 
				
			|||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
					      "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					      "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					      "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -101,6 +105,8 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
				
			||||||
@ -114,7 +120,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
@ -155,6 +163,8 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
				
			||||||
@ -168,7 +178,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
@ -273,6 +285,8 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -298,7 +312,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
@ -342,6 +358,8 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -367,7 +385,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
@ -424,6 +444,8 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -449,7 +471,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
@ -498,7 +522,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
					  "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,9 @@ FROM
 | 
				
			|||||||
      "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
 | 
					      "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
 | 
				
			||||||
      "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
 | 
					      "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
 | 
				
			||||||
      "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
 | 
					      "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
 | 
				
			||||||
      "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled"
 | 
					      "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled",
 | 
				
			||||||
 | 
					      "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes"
 | 
				
			||||||
    FROM
 | 
					    FROM
 | 
				
			||||||
      "api_keys" "APIKeyEntity"
 | 
					      "api_keys" "APIKeyEntity"
 | 
				
			||||||
      LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId"
 | 
					      LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId"
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,9 @@ FROM
 | 
				
			|||||||
      "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
					      "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
				
			||||||
      "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
					      "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
				
			||||||
      "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
					      "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
				
			||||||
      "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
 | 
					      "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					      "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
    FROM
 | 
					    FROM
 | 
				
			||||||
      "libraries" "LibraryEntity"
 | 
					      "libraries" "LibraryEntity"
 | 
				
			||||||
      LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
					      LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
				
			||||||
@ -144,7 +146,9 @@ SELECT
 | 
				
			|||||||
  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
 | 
					  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "libraries" "LibraryEntity"
 | 
					  "libraries" "LibraryEntity"
 | 
				
			||||||
  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
					  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
				
			||||||
@ -188,7 +192,9 @@ SELECT
 | 
				
			|||||||
  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
 | 
					  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "libraries" "LibraryEntity"
 | 
					  "libraries" "LibraryEntity"
 | 
				
			||||||
  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
					  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
				
			||||||
@ -226,7 +232,9 @@ SELECT
 | 
				
			|||||||
  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
					  "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
 | 
				
			||||||
  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
 | 
					  "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "libraries" "LibraryEntity"
 | 
					  "libraries" "LibraryEntity"
 | 
				
			||||||
  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
					  LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
 | 
				
			||||||
 | 
				
			|||||||
@ -155,7 +155,9 @@ FROM
 | 
				
			|||||||
      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
 | 
				
			||||||
      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
 | 
				
			||||||
      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
 | 
				
			||||||
      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled"
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
 | 
				
			||||||
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
 | 
				
			||||||
    FROM
 | 
					    FROM
 | 
				
			||||||
      "shared_links" "SharedLinkEntity"
 | 
					      "shared_links" "SharedLinkEntity"
 | 
				
			||||||
      LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
 | 
					      LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
 | 
				
			||||||
@ -257,7 +259,9 @@ SELECT
 | 
				
			|||||||
  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
 | 
				
			||||||
  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
 | 
				
			||||||
  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
 | 
				
			||||||
  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled"
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
 | 
				
			||||||
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "shared_links" "SharedLinkEntity"
 | 
					  "shared_links" "SharedLinkEntity"
 | 
				
			||||||
  LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
 | 
					  LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
 | 
				
			||||||
@ -309,7 +313,9 @@ FROM
 | 
				
			|||||||
      "SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
 | 
				
			||||||
      "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
 | 
				
			||||||
      "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
 | 
				
			||||||
      "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled"
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled",
 | 
				
			||||||
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes"
 | 
				
			||||||
    FROM
 | 
					    FROM
 | 
				
			||||||
      "shared_links" "SharedLinkEntity"
 | 
					      "shared_links" "SharedLinkEntity"
 | 
				
			||||||
      LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId"
 | 
					      LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId"
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,9 @@ SELECT
 | 
				
			|||||||
  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
					  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
				
			||||||
  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
					  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
				
			||||||
  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
					  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
				
			||||||
  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
 | 
					  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "users" "UserEntity"
 | 
					  "users" "UserEntity"
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
@ -60,7 +62,9 @@ SELECT
 | 
				
			|||||||
  "user"."createdAt" AS "user_createdAt",
 | 
					  "user"."createdAt" AS "user_createdAt",
 | 
				
			||||||
  "user"."deletedAt" AS "user_deletedAt",
 | 
					  "user"."deletedAt" AS "user_deletedAt",
 | 
				
			||||||
  "user"."updatedAt" AS "user_updatedAt",
 | 
					  "user"."updatedAt" AS "user_updatedAt",
 | 
				
			||||||
  "user"."memoriesEnabled" AS "user_memoriesEnabled"
 | 
					  "user"."memoriesEnabled" AS "user_memoriesEnabled",
 | 
				
			||||||
 | 
					  "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "users" "user"
 | 
					  "users" "user"
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
@ -82,7 +86,9 @@ SELECT
 | 
				
			|||||||
  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
					  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
				
			||||||
  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
					  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
				
			||||||
  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
					  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
				
			||||||
  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
 | 
					  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "users" "UserEntity"
 | 
					  "users" "UserEntity"
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
@ -106,7 +112,9 @@ SELECT
 | 
				
			|||||||
  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
					  "UserEntity"."createdAt" AS "UserEntity_createdAt",
 | 
				
			||||||
  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
					  "UserEntity"."deletedAt" AS "UserEntity_deletedAt",
 | 
				
			||||||
  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
					  "UserEntity"."updatedAt" AS "UserEntity_updatedAt",
 | 
				
			||||||
  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
 | 
					  "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "users" "UserEntity"
 | 
					  "users" "UserEntity"
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
@ -119,6 +127,7 @@ LIMIT
 | 
				
			|||||||
SELECT
 | 
					SELECT
 | 
				
			||||||
  "users"."id" AS "userId",
 | 
					  "users"."id" AS "userId",
 | 
				
			||||||
  "users"."name" AS "userName",
 | 
					  "users"."name" AS "userName",
 | 
				
			||||||
 | 
					  "users"."quotaSizeInBytes" AS "quotaSizeInBytes",
 | 
				
			||||||
  COUNT("assets"."id") FILTER (
 | 
					  COUNT("assets"."id") FILTER (
 | 
				
			||||||
    WHERE
 | 
					    WHERE
 | 
				
			||||||
      "assets"."type" = 'IMAGE'
 | 
					      "assets"."type" = 'IMAGE'
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,9 @@ FROM
 | 
				
			|||||||
      "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
 | 
					      "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
 | 
				
			||||||
      "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
 | 
					      "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
 | 
				
			||||||
      "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
 | 
					      "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
 | 
				
			||||||
      "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled"
 | 
					      "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled",
 | 
				
			||||||
 | 
					      "UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes",
 | 
				
			||||||
 | 
					      "UserTokenEntity__UserTokenEntity_user"."quotaUsageInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaUsageInBytes"
 | 
				
			||||||
    FROM
 | 
					    FROM
 | 
				
			||||||
      "user_token" "UserTokenEntity"
 | 
					      "user_token" "UserTokenEntity"
 | 
				
			||||||
      LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId"
 | 
					      LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId"
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,7 @@ export class AppService {
 | 
				
			|||||||
      [JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
 | 
					      [JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
 | 
				
			||||||
      [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
 | 
					      [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
 | 
				
			||||||
      [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
 | 
					      [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
 | 
				
			||||||
 | 
					      [JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(),
 | 
				
			||||||
      [JobName.QUEUE_ENCODE_CLIP]: (data) => this.smartInfoService.handleQueueEncodeClip(data),
 | 
					      [JobName.QUEUE_ENCODE_CLIP]: (data) => this.smartInfoService.handleQueueEncodeClip(data),
 | 
				
			||||||
      [JobName.ENCODE_CLIP]: (data) => this.smartInfoService.handleEncodeClip(data),
 | 
					      [JobName.ENCODE_CLIP]: (data) => this.smartInfoService.handleEncodeClip(data),
 | 
				
			||||||
      [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
 | 
					      [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								server/test/fixtures/file.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								server/test/fixtures/file.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -4,11 +4,13 @@ export const fileStub = {
 | 
				
			|||||||
    originalPath: 'fake_path/asset_1.jpeg',
 | 
					    originalPath: 'fake_path/asset_1.jpeg',
 | 
				
			||||||
    checksum: Buffer.from('file hash', 'utf8'),
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
    originalName: 'asset_1.jpeg',
 | 
					    originalName: 'asset_1.jpeg',
 | 
				
			||||||
 | 
					    size: 42,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  livePhotoMotion: Object.freeze({
 | 
					  livePhotoMotion: Object.freeze({
 | 
				
			||||||
    uuid: 'random-uuid',
 | 
					    uuid: 'random-uuid',
 | 
				
			||||||
    originalPath: 'fake_path/asset_1.mp4',
 | 
					    originalPath: 'fake_path/asset_1.mp4',
 | 
				
			||||||
    checksum: Buffer.from('live photo file hash', 'utf8'),
 | 
					    checksum: Buffer.from('live photo file hash', 'utf8'),
 | 
				
			||||||
    originalName: 'asset_1.mp4',
 | 
					    originalName: 'asset_1.mp4',
 | 
				
			||||||
 | 
					    size: 69,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								server/test/fixtures/user.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								server/test/fixtures/user.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -17,6 +17,12 @@ export const userDto = {
 | 
				
			|||||||
    password: 'Password123',
 | 
					    password: 'Password123',
 | 
				
			||||||
    name: 'User 3',
 | 
					    name: 'User 3',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  userWithQuota: {
 | 
				
			||||||
 | 
					    email: 'quota-user@immich.app',
 | 
				
			||||||
 | 
					    password: 'Password123',
 | 
				
			||||||
 | 
					    name: 'User with quota',
 | 
				
			||||||
 | 
					    quotaSizeInBytes: 42,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const userStub = {
 | 
					export const userStub = {
 | 
				
			||||||
@ -36,6 +42,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  user1: Object.freeze<UserEntity>({
 | 
					  user1: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user1.user,
 | 
					    ...authStub.user1.user,
 | 
				
			||||||
@ -53,6 +61,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  user2: Object.freeze<UserEntity>({
 | 
					  user2: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user2.user,
 | 
					    ...authStub.user2.user,
 | 
				
			||||||
@ -70,6 +80,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  storageLabel: Object.freeze<UserEntity>({
 | 
					  storageLabel: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user1.user,
 | 
					    ...authStub.user1.user,
 | 
				
			||||||
@ -87,6 +99,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  externalPath1: Object.freeze<UserEntity>({
 | 
					  externalPath1: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user1.user,
 | 
					    ...authStub.user1.user,
 | 
				
			||||||
@ -104,6 +118,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  externalPath2: Object.freeze<UserEntity>({
 | 
					  externalPath2: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user1.user,
 | 
					    ...authStub.user1.user,
 | 
				
			||||||
@ -121,6 +137,8 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  profilePath: Object.freeze<UserEntity>({
 | 
					  profilePath: Object.freeze<UserEntity>({
 | 
				
			||||||
    ...authStub.user1.user,
 | 
					    ...authStub.user1.user,
 | 
				
			||||||
@ -138,5 +156,7 @@ export const userStub = {
 | 
				
			|||||||
    assets: [],
 | 
					    assets: [],
 | 
				
			||||||
    memoriesEnabled: true,
 | 
					    memoriesEnabled: true,
 | 
				
			||||||
    avatarColor: UserAvatarColor.PRIMARY,
 | 
					    avatarColor: UserAvatarColor.PRIMARY,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -19,5 +19,7 @@ export const newUserRepositoryMock = (reset = true): jest.Mocked<IUserRepository
 | 
				
			|||||||
    getDeletedUsers: jest.fn(),
 | 
					    getDeletedUsers: jest.fn(),
 | 
				
			||||||
    restore: jest.fn(),
 | 
					    restore: jest.fn(),
 | 
				
			||||||
    hasAdmin: jest.fn(),
 | 
					    hasAdmin: jest.fn(),
 | 
				
			||||||
 | 
					    updateUsage: jest.fn(),
 | 
				
			||||||
 | 
					    syncUsage: jest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
  import type { ServerStatsResponseDto } from '@api';
 | 
					  import type { ServerStatsResponseDto } from '@api';
 | 
				
			||||||
  import { asByteUnitString, getBytesWithUnit } from '$lib/utils/byte-units';
 | 
					  import { asByteUnitString, getBytesWithUnit } from '$lib/utils/byte-units';
 | 
				
			||||||
  import StatsCard from './stats-card.svelte';
 | 
					  import StatsCard from './stats-card.svelte';
 | 
				
			||||||
  import { mdiCameraIris, mdiMemory, mdiPlayCircle } from '@mdi/js';
 | 
					  import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let stats: ServerStatsResponseDto = {
 | 
					  export let stats: ServerStatsResponseDto = {
 | 
				
			||||||
@ -32,7 +32,7 @@
 | 
				
			|||||||
    <div class="mt-5 hidden justify-between lg:flex">
 | 
					    <div class="mt-5 hidden justify-between lg:flex">
 | 
				
			||||||
      <StatsCard icon={mdiCameraIris} title="PHOTOS" value={stats.photos} />
 | 
					      <StatsCard icon={mdiCameraIris} title="PHOTOS" value={stats.photos} />
 | 
				
			||||||
      <StatsCard icon={mdiPlayCircle} title="VIDEOS" value={stats.videos} />
 | 
					      <StatsCard icon={mdiPlayCircle} title="VIDEOS" value={stats.videos} />
 | 
				
			||||||
      <StatsCard icon={mdiMemory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
 | 
					      <StatsCard icon={mdiChartPie} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="mt-5 flex lg:hidden">
 | 
					    <div class="mt-5 flex lg:hidden">
 | 
				
			||||||
      <div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
 | 
					      <div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
 | 
				
			||||||
@ -62,7 +62,7 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="flex flex-wrap gap-x-7">
 | 
					        <div class="flex flex-wrap gap-x-7">
 | 
				
			||||||
          <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
 | 
					          <div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
 | 
				
			||||||
            <Icon path={mdiMemory} size="25" />
 | 
					            <Icon path={mdiChartPie} size="25" />
 | 
				
			||||||
            <p>STORAGE</p>
 | 
					            <p>STORAGE</p>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -87,7 +87,7 @@
 | 
				
			|||||||
          <th class="w-1/4 text-center text-sm font-medium">User</th>
 | 
					          <th class="w-1/4 text-center text-sm font-medium">User</th>
 | 
				
			||||||
          <th class="w-1/4 text-center text-sm font-medium">Photos</th>
 | 
					          <th class="w-1/4 text-center text-sm font-medium">Photos</th>
 | 
				
			||||||
          <th class="w-1/4 text-center text-sm font-medium">Videos</th>
 | 
					          <th class="w-1/4 text-center text-sm font-medium">Videos</th>
 | 
				
			||||||
          <th class="w-1/4 text-center text-sm font-medium">Size</th>
 | 
					          <th class="w-1/4 text-center text-sm font-medium">Usage</th>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
      </thead>
 | 
					      </thead>
 | 
				
			||||||
      <tbody
 | 
					      <tbody
 | 
				
			||||||
@ -100,7 +100,16 @@
 | 
				
			|||||||
            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
 | 
					            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
 | 
				
			||||||
            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td>
 | 
					            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td>
 | 
				
			||||||
            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
 | 
					            <td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
 | 
				
			||||||
            <td class="w-1/4 text-ellipsis px-2 text-sm">{asByteUnitString(user.usage, $locale)}</td>
 | 
					            <td class="w-1/4 text-ellipsis px-2 text-sm">
 | 
				
			||||||
 | 
					              {asByteUnitString(user.usage, $locale, 0)}
 | 
				
			||||||
 | 
					              <span class="text-immich-primary dark:text-immich-dark-primary">
 | 
				
			||||||
 | 
					                {#if user.quotaSizeInBytes}
 | 
				
			||||||
 | 
					                  ({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%)
 | 
				
			||||||
 | 
					                {:else}
 | 
				
			||||||
 | 
					                  (Unlimited)
 | 
				
			||||||
 | 
					                {/if}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        {/each}
 | 
					        {/each}
 | 
				
			||||||
      </tbody>
 | 
					      </tbody>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@
 | 
				
			|||||||
  import ImmichLogo from '../shared-components/immich-logo.svelte';
 | 
					  import ImmichLogo from '../shared-components/immich-logo.svelte';
 | 
				
			||||||
  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
					  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
				
			||||||
  import Button from '../elements/buttons/button.svelte';
 | 
					  import Button from '../elements/buttons/button.svelte';
 | 
				
			||||||
 | 
					  import { convertToBytes } from '$lib/utils/byte-converter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let error: string;
 | 
					  let error: string;
 | 
				
			||||||
  let success: string;
 | 
					  let success: string;
 | 
				
			||||||
@ -42,6 +43,7 @@
 | 
				
			|||||||
      const email = form.get('email');
 | 
					      const email = form.get('email');
 | 
				
			||||||
      const password = form.get('password');
 | 
					      const password = form.get('password');
 | 
				
			||||||
      const name = form.get('name');
 | 
					      const name = form.get('name');
 | 
				
			||||||
 | 
					      const quotaSize = form.get('quotaSize');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { status } = await api.userApi.createUser({
 | 
					        const { status } = await api.userApi.createUser({
 | 
				
			||||||
@ -49,6 +51,7 @@
 | 
				
			|||||||
            email: String(email),
 | 
					            email: String(email),
 | 
				
			||||||
            password: String(password),
 | 
					            password: String(password),
 | 
				
			||||||
            name: String(name),
 | 
					            name: String(name),
 | 
				
			||||||
 | 
					            quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -117,6 +120,11 @@
 | 
				
			|||||||
      <input class="immich-form-input" id="name" name="name" type="text" required />
 | 
					      <input class="immich-form-input" id="name" name="name" type="text" required />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
 | 
					      <label class="immich-form-label" for="quotaSize">Quota Size (GB)</label>
 | 
				
			||||||
 | 
					      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if error}
 | 
					    {#if error}
 | 
				
			||||||
      <p class="ml-4 text-sm text-red-400">{error}</p>
 | 
					      <p class="ml-4 text-sm text-red-400">{error}</p>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,12 @@
 | 
				
			|||||||
  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
					  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
				
			||||||
  import Button from '../elements/buttons/button.svelte';
 | 
					  import Button from '../elements/buttons/button.svelte';
 | 
				
			||||||
  import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
 | 
					  import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
 | 
				
			||||||
  import { handleError } from '../../utils/handle-error';
 | 
					 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
 | 
					  import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					  import { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let user: UserResponseDto;
 | 
					  export let user: UserResponseDto;
 | 
				
			||||||
  export let canResetPassword = true;
 | 
					  export let canResetPassword = true;
 | 
				
			||||||
@ -24,6 +25,8 @@
 | 
				
			|||||||
    editSuccess: void;
 | 
					    editSuccess: void;
 | 
				
			||||||
  }>();
 | 
					  }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let quotaSize = user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, 'GiB') : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const editUser = async () => {
 | 
					  const editUser = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { id, email, name, storageLabel, externalPath } = user;
 | 
					      const { id, email, name, storageLabel, externalPath } = user;
 | 
				
			||||||
@ -34,6 +37,7 @@
 | 
				
			|||||||
          name,
 | 
					          name,
 | 
				
			||||||
          storageLabel: storageLabel || '',
 | 
					          storageLabel: storageLabel || '',
 | 
				
			||||||
          externalPath: externalPath || '',
 | 
					          externalPath: externalPath || '',
 | 
				
			||||||
 | 
					          quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), 'GiB') : null,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -97,6 +101,11 @@
 | 
				
			|||||||
      <input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
 | 
					      <input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
 | 
					      <label class="immich-form-label" for="quotaSize">Quota Size (GB)</label>
 | 
				
			||||||
 | 
					      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="m-4 flex flex-col gap-2">
 | 
					    <div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
      <label class="immich-form-label" for="storage-label">Storage Label</label>
 | 
					      <label class="immich-form-label" for="storage-label">Storage Label</label>
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,43 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { browser } from '$app/environment';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { websocketStore } from '$lib/stores/websocket';
 | 
					  import { websocketStore } from '$lib/stores/websocket';
 | 
				
			||||||
  import { api } from '@api';
 | 
					  import { UserResponseDto, api } from '@api';
 | 
				
			||||||
  import { onDestroy, onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					 | 
				
			||||||
  import { asByteUnitString } from '../../utils/byte-units';
 | 
					  import { asByteUnitString } from '../../utils/byte-units';
 | 
				
			||||||
  import LoadingSpinner from './loading-spinner.svelte';
 | 
					  import LoadingSpinner from './loading-spinner.svelte';
 | 
				
			||||||
  import { mdiCloud, mdiDns } from '@mdi/js';
 | 
					  import { mdiChartPie, mdiDns } from '@mdi/js';
 | 
				
			||||||
  import { serverInfoStore } from '$lib/stores/server-info.store';
 | 
					  import { serverInfoStore } from '$lib/stores/server-info.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { serverVersion, connected } = websocketStore;
 | 
					  const { serverVersion, connected } = websocketStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let userInfo: UserResponseDto;
 | 
				
			||||||
 | 
					  let usageClasses = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
 | 
					  $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
 | 
				
			||||||
  $: usedPercentage = Math.round(($serverInfoStore?.diskUseRaw / $serverInfoStore?.diskSizeRaw) * 100);
 | 
					  $: hasQuota = userInfo?.quotaSizeInBytes !== null;
 | 
				
			||||||
 | 
					  $: availableBytes = (hasQuota ? userInfo?.quotaSizeInBytes : $serverInfoStore.diskSizeRaw) || 0;
 | 
				
			||||||
 | 
					  $: usedBytes = (hasQuota ? userInfo?.quotaUsageInBytes : $serverInfoStore.diskUseRaw) || 0;
 | 
				
			||||||
 | 
					  $: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onUpdate = () => {
 | 
				
			||||||
 | 
					    usedPercentage = 81;
 | 
				
			||||||
 | 
					    usageClasses = getUsageClass();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getUsageClass = () => {
 | 
				
			||||||
 | 
					    if (usedPercentage >= 95) {
 | 
				
			||||||
 | 
					      return 'bg-red-500';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (usedPercentage > 80) {
 | 
				
			||||||
 | 
					      return 'bg-yellow-500';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 'bg-immich-primary dark:bg-immich-dark-primary';
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: userInfo && onUpdate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMount(async () => {
 | 
					  onMount(async () => {
 | 
				
			||||||
    await refresh();
 | 
					    await refresh();
 | 
				
			||||||
@ -21,39 +45,33 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const refresh = async () => {
 | 
					  const refresh = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { data } = await api.serverInfoApi.getServerInfo();
 | 
					      [$serverInfoStore, userInfo] = await Promise.all([
 | 
				
			||||||
      $serverInfoStore = data;
 | 
					        api.serverInfoApi.getServerInfo().then(({ data }) => data),
 | 
				
			||||||
 | 
					        api.userApi.getMyUserInfo().then(({ data }) => data),
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log('Error [StatusBox] [onMount]');
 | 
					      console.log('Error [StatusBox] [onMount]');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let interval: number;
 | 
					 | 
				
			||||||
  if (browser) {
 | 
					 | 
				
			||||||
    interval = window.setInterval(() => refresh(), 10_000);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDestroy(() => clearInterval(interval));
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="dark:text-immich-dark-fg">
 | 
					<div class="dark:text-immich-dark-fg">
 | 
				
			||||||
  <div class="storage-status grid grid-cols-[64px_auto]">
 | 
					  <div
 | 
				
			||||||
 | 
					    class="storage-status grid grid-cols-[64px_auto]"
 | 
				
			||||||
 | 
					    title="Used {asByteUnitString(usedBytes, $locale, 3)} of {asByteUnitString(availableBytes, $locale, 3)}"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
    <div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
 | 
					    <div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
 | 
				
			||||||
      <Icon path={mdiCloud} size={'24'} />
 | 
					      <Icon path={mdiChartPie} size="24" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="hidden group-hover:sm:block md:block">
 | 
					    <div class="hidden group-hover:sm:block md:block">
 | 
				
			||||||
      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
 | 
					      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
 | 
				
			||||||
      {#if $serverInfoStore}
 | 
					      {#if $serverInfoStore}
 | 
				
			||||||
        <div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
 | 
					        <div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
 | 
				
			||||||
          <!-- style={`width: ${$downloadAssets[fileName]}%`} -->
 | 
					          <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            class="h-[7px] rounded-full bg-immich-primary dark:bg-immich-dark-primary"
 | 
					 | 
				
			||||||
            style="width: {usedPercentage}%"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <p class="text-xs">
 | 
					        <p class="text-xs">
 | 
				
			||||||
          {asByteUnitString($serverInfoStore?.diskUseRaw, $locale)} of
 | 
					          {asByteUnitString(usedBytes, $locale)} of
 | 
				
			||||||
          {asByteUnitString($serverInfoStore?.diskSizeRaw, $locale)} used
 | 
					          {asByteUnitString(availableBytes, $locale)} used
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
      {:else}
 | 
					      {:else}
 | 
				
			||||||
        <div class="mt-2">
 | 
					        <div class="mt-2">
 | 
				
			||||||
@ -67,7 +85,7 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="server-status grid grid-cols-[64px_auto]">
 | 
					  <div class="server-status grid grid-cols-[64px_auto]">
 | 
				
			||||||
    <div class="pb-11 pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
 | 
					    <div class="pb-11 pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
 | 
				
			||||||
      <Icon path={mdiDns} size={'24'} />
 | 
					      <Icon path={mdiDns} size="26" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="hidden text-xs group-hover:sm:block md:block">
 | 
					    <div class="hidden text-xs group-hover:sm:block md:block">
 | 
				
			||||||
      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Server</p>
 | 
					      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Server</p>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										37
									
								
								web/src/lib/utils/byte-converter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/lib/utils/byte-converter.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Convert to bytes from on a specified unit.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * * `1, 'GiB'`, returns `1073741824` bytes
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param size value to be converted
 | 
				
			||||||
 | 
					 * @param unit unit to convert from
 | 
				
			||||||
 | 
					 * @returns bytes (number)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function convertToBytes(size: number, unit: string): number {
 | 
				
			||||||
 | 
					  let bytes = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (unit === 'GiB') {
 | 
				
			||||||
 | 
					    bytes = size * 1073741824;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return bytes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Convert from bytes to a specified unit.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * * `11073741824, 'GiB'`, returns `1` GiB
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param bytes value to be converted
 | 
				
			||||||
 | 
					 * @param unit unit to convert to
 | 
				
			||||||
 | 
					 * @returns bytes (number)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function convertFromBytes(bytes: number, unit: string): number {
 | 
				
			||||||
 | 
					  let size = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (unit === 'GiB') {
 | 
				
			||||||
 | 
					    size = bytes / 1073741824;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return size;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { uploadAssetsStore } from '$lib/stores/upload';
 | 
					import { uploadAssetsStore } from '$lib/stores/upload';
 | 
				
			||||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 | 
					import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
import { api, AssetFileUploadResponseDto } from '@api';
 | 
					import { api, AssetFileUploadResponseDto } from '@api';
 | 
				
			||||||
import { notificationController, NotificationType } from './../components/shared-components/notification/notification';
 | 
					 | 
				
			||||||
import { UploadState } from '$lib/models/upload-asset';
 | 
					import { UploadState } from '$lib/models/upload-asset';
 | 
				
			||||||
import { ExecutorQueue } from '$lib/utils/executor-queue';
 | 
					import { ExecutorQueue } from '$lib/utils/executor-queue';
 | 
				
			||||||
 | 
					import { getServerErrorMessage, handleError } from './handle-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let _extensions: string[];
 | 
					let _extensions: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -115,26 +115,10 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
 | 
				
			|||||||
        return res.id;
 | 
					        return res.id;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .catch((reason) => {
 | 
					    .catch(async (error) => {
 | 
				
			||||||
      console.log('error uploading file ', reason);
 | 
					      await handleError(error, 'Unable to upload file');
 | 
				
			||||||
 | 
					      const reason = (await getServerErrorMessage(error)) || error;
 | 
				
			||||||
      uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
 | 
					      uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
 | 
				
			||||||
      handleUploadError(asset, JSON.stringify(reason));
 | 
					 | 
				
			||||||
      return undefined;
 | 
					      return undefined;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
function handleUploadError(asset: File, respBody = '{}', extraMessage?: string) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = JSON.parse(respBody);
 | 
					 | 
				
			||||||
    const extraMsg = res ? ' ' + res?.message : '';
 | 
					 | 
				
			||||||
    const messageSuffix = extraMessage !== undefined ? ` ${extraMessage}` : '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    notificationController.show({
 | 
					 | 
				
			||||||
      type: NotificationType.Error,
 | 
					 | 
				
			||||||
      message: `Cannot upload file ${asset.name} ${extraMsg}${messageSuffix}`,
 | 
					 | 
				
			||||||
      timeout: 5000,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error('ERROR parsing data JSON in handleUploadError');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -17,4 +17,6 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
 | 
				
			|||||||
  memoriesEnabled: true,
 | 
					  memoriesEnabled: true,
 | 
				
			||||||
  oauthId: '',
 | 
					  oauthId: '',
 | 
				
			||||||
  avatarColor: UserAvatarColor.Primary,
 | 
					  avatarColor: UserAvatarColor.Primary,
 | 
				
			||||||
 | 
					  quotaUsageInBytes: 0,
 | 
				
			||||||
 | 
					  quotaSizeInBytes: null,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user