diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index d3d7db6972..a1b717883d 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -1,6 +1,7 @@ import { ActivityCreateDto, AlbumResponseDto, + AlbumUserRole, AssetFileUploadResponseDto, LoginResponseDto, ReactionType, @@ -33,7 +34,7 @@ describe('/activity', () => { createAlbumDto: { albumName: 'Album 1', assetIds: [asset.id], - sharedWithUserIds: [nonOwner.userId], + albumUsers: [{ userId: nonOwner.userId, role: AlbumUserRole.Editor }], }, }, { headers: asBearerAuth(admin.accessToken) }, diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index a3459bea3d..f7d05aac50 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -49,72 +49,50 @@ describe('/album', () => { utils.createAsset(user1.accessToken), ]); - const albums = await Promise.all([ - // user 1 - /* 0 */ + user1Albums = await Promise.all([ utils.createAlbum(user1.accessToken, { albumName: user1SharedEditorUser, - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }], assetIds: [user1Asset1.id], }), - /* 1 */ utils.createAlbum(user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset1.id], }), - /* 2 */ utils.createAlbum(user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset1.id, user1Asset2.id], }), - - // user 2 - /* 3 */ - utils.createAlbum(user2.accessToken, { - albumName: user2SharedUser, - sharedWithUserIds: [user1.userId, user3.userId], - }), - /* 4 */ - utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), - /* 5 */ - utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), - - // user 3 - /* 6 */ - utils.createAlbum(user3.accessToken, { - albumName: 'Deleted', - sharedWithUserIds: [user1.userId], - }), - - // user1 shared with an editor - /* 7 */ utils.createAlbum(user1.accessToken, { albumName: user1SharedViewerUser, - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], assetIds: [user1Asset1.id], }), ]); - // Make viewer - await utils.updateAlbumUser(user1.accessToken, { - id: albums[7].id, - userId: user2.userId, - updateAlbumUserDto: { role: AlbumUserRole.Viewer }, + user2Albums = await Promise.all([ + utils.createAlbum(user2.accessToken, { + albumName: user2SharedUser, + albumUsers: [ + { userId: user1.userId, role: AlbumUserRole.Editor }, + { userId: user3.userId, role: AlbumUserRole.Editor }, + ], + }), + utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + ]); + + await utils.createAlbum(user3.accessToken, { + albumName: 'Deleted', + albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }], }); - albums[0].albumUsers[0].role = AlbumUserRole.Editor; - albums[3].albumUsers[0].role = AlbumUserRole.Editor; - albums[6].albumUsers[0].role = AlbumUserRole.Editor; - await addAssetsToAlbum( - { id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } }, + { id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } }, { headers: asBearerAuth(user1.accessToken) }, ); - albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) }); - - user1Albums = [...albums.slice(0, 3), albums[7]]; - user2Albums = albums.slice(3, 6); + user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }); await Promise.all([ // add shared link to user1SharedLink album @@ -641,9 +619,11 @@ describe('/album', () => { it('should allow the album owner to change the role of a shared user', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], }); + expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); + const { status } = await request(app) .put(`/album/${album.id}/user/${user2.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -663,9 +643,11 @@ describe('/album', () => { it('should not allow a shared user to change the role of another shared user', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], }); + expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); + const { status, body } = await request(app) .put(`/album/${album.id}/user/${user2.userId}`) .set('Authorization', `Bearer ${user2.accessToken}`) diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 65cf5428f8..570132ada5 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -18,6 +18,7 @@ doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md doc/AlbumUserAddDto.md +doc/AlbumUserCreateDto.md doc/AlbumUserResponseDto.md doc/AlbumUserRole.md doc/AllJobStatusResponseDto.md @@ -257,6 +258,7 @@ lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/album_user_add_dto.dart +lib/model/album_user_create_dto.dart lib/model/album_user_response_dto.dart lib/model/album_user_role.dart lib/model/all_job_status_response_dto.dart @@ -444,6 +446,7 @@ test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart test/album_user_add_dto_test.dart +test/album_user_create_dto_test.dart test/album_user_response_dto_test.dart test/album_user_role_test.dart test/all_job_status_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 65ecb51b9b..c98745430c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -235,6 +235,7 @@ Class | Method | HTTP request | Description - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AlbumUserAddDto](doc//AlbumUserAddDto.md) + - [AlbumUserCreateDto](doc//AlbumUserCreateDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) - [AlbumUserRole](doc//AlbumUserRole.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) diff --git a/mobile/openapi/doc/AlbumUserCreateDto.md b/mobile/openapi/doc/AlbumUserCreateDto.md new file mode 100644 index 0000000000..78acbc476b --- /dev/null +++ b/mobile/openapi/doc/AlbumUserCreateDto.md @@ -0,0 +1,16 @@ +# openapi.model.AlbumUserCreateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**role** | [**AlbumUserRole**](AlbumUserRole.md) | | +**userId** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/CreateAlbumDto.md b/mobile/openapi/doc/CreateAlbumDto.md index 0a472725e4..34035d4af6 100644 --- a/mobile/openapi/doc/CreateAlbumDto.md +++ b/mobile/openapi/doc/CreateAlbumDto.md @@ -9,9 +9,10 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **albumName** | **String** | | +**albumUsers** | [**List**](AlbumUserCreateDto.md) | This property was added in v1.104.0 | [optional] [default to const []] **assetIds** | **List** | | [optional] [default to const []] **description** | **String** | | [optional] -**sharedWithUserIds** | **List** | | [optional] [default to const []] +**sharedWithUserIds** | **List** | This property was deprecated in v1.104.0 | [optional] [default to const []] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 555bad2ee6..02da5876dc 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -68,6 +68,7 @@ part 'model/admin_onboarding_update_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/album_user_add_dto.dart'; +part 'model/album_user_create_dto.dart'; part 'model/album_user_response_dto.dart'; part 'model/album_user_role.dart'; part 'model/all_job_status_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f1105f6f5f..3b21ff6e0f 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -206,6 +206,8 @@ class ApiClient { return AlbumResponseDto.fromJson(value); case 'AlbumUserAddDto': return AlbumUserAddDto.fromJson(value); + case 'AlbumUserCreateDto': + return AlbumUserCreateDto.fromJson(value); case 'AlbumUserResponseDto': return AlbumUserResponseDto.fromJson(value); case 'AlbumUserRole': diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart new file mode 100644 index 0000000000..4c8f2ec6d1 --- /dev/null +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AlbumUserCreateDto { + /// Returns a new [AlbumUserCreateDto] instance. + AlbumUserCreateDto({ + required this.role, + required this.userId, + }); + + AlbumUserRole role; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is AlbumUserCreateDto && + other.role == role && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (role.hashCode) + + (userId.hashCode); + + @override + String toString() => 'AlbumUserCreateDto[role=$role, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'role'] = this.role; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [AlbumUserCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AlbumUserCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AlbumUserCreateDto( + role: AlbumUserRole.fromJson(json[r'role'])!, + userId: mapValueOfType(json, r'userId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AlbumUserCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AlbumUserCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AlbumUserCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AlbumUserCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'role', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index 7b527eb7bc..7af3526a45 100644 --- a/mobile/openapi/lib/model/create_album_dto.dart +++ b/mobile/openapi/lib/model/create_album_dto.dart @@ -14,6 +14,7 @@ class CreateAlbumDto { /// Returns a new [CreateAlbumDto] instance. CreateAlbumDto({ required this.albumName, + this.albumUsers = const [], this.assetIds = const [], this.description, this.sharedWithUserIds = const [], @@ -21,6 +22,9 @@ class CreateAlbumDto { String albumName; + /// This property was added in v1.104.0 + List albumUsers; + List assetIds; /// @@ -31,11 +35,13 @@ class CreateAlbumDto { /// String? description; + /// This property was deprecated in v1.104.0 List sharedWithUserIds; @override bool operator ==(Object other) => identical(this, other) || other is CreateAlbumDto && other.albumName == albumName && + _deepEquality.equals(other.albumUsers, albumUsers) && _deepEquality.equals(other.assetIds, assetIds) && other.description == description && _deepEquality.equals(other.sharedWithUserIds, sharedWithUserIds); @@ -44,16 +50,18 @@ class CreateAlbumDto { int get hashCode => // ignore: unnecessary_parenthesis (albumName.hashCode) + + (albumUsers.hashCode) + (assetIds.hashCode) + (description == null ? 0 : description!.hashCode) + (sharedWithUserIds.hashCode); @override - String toString() => 'CreateAlbumDto[albumName=$albumName, assetIds=$assetIds, description=$description, sharedWithUserIds=$sharedWithUserIds]'; + String toString() => 'CreateAlbumDto[albumName=$albumName, albumUsers=$albumUsers, assetIds=$assetIds, description=$description, sharedWithUserIds=$sharedWithUserIds]'; Map toJson() { final json = {}; json[r'albumName'] = this.albumName; + json[r'albumUsers'] = this.albumUsers; json[r'assetIds'] = this.assetIds; if (this.description != null) { json[r'description'] = this.description; @@ -73,6 +81,7 @@ class CreateAlbumDto { return CreateAlbumDto( albumName: mapValueOfType(json, r'albumName')!, + albumUsers: AlbumUserCreateDto.listFromJson(json[r'albumUsers']), assetIds: json[r'assetIds'] is Iterable ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], diff --git a/mobile/openapi/test/album_user_create_dto_test.dart b/mobile/openapi/test/album_user_create_dto_test.dart new file mode 100644 index 0000000000..a1459172f7 --- /dev/null +++ b/mobile/openapi/test/album_user_create_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AlbumUserCreateDto +void main() { + // final instance = AlbumUserCreateDto(); + + group('test AlbumUserCreateDto', () { + // AlbumUserRole role + test('to test the property `role`', () async { + // TODO + }); + + // String userId + test('to test the property `userId`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/create_album_dto_test.dart b/mobile/openapi/test/create_album_dto_test.dart index d23e66cf7e..f3dc3c8647 100644 --- a/mobile/openapi/test/create_album_dto_test.dart +++ b/mobile/openapi/test/create_album_dto_test.dart @@ -21,6 +21,12 @@ void main() { // TODO }); + // This property was added in v1.104.0 + // List albumUsers (default value: const []) + test('to test the property `albumUsers`', () async { + // TODO + }); + // List assetIds (default value: const []) test('to test the property `assetIds`', () async { // TODO @@ -31,6 +37,7 @@ void main() { // TODO }); + // This property was deprecated in v1.104.0 // List sharedWithUserIds (default value: const []) test('to test the property `sharedWithUserIds`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ae6a3dfb06..993705f97a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6779,6 +6779,22 @@ ], "type": "object" }, + "AlbumUserCreateDto": { + "properties": { + "role": { + "$ref": "#/components/schemas/AlbumUserRole" + }, + "userId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "role", + "userId" + ], + "type": "object" + }, "AlbumUserResponseDto": { "properties": { "role": { @@ -7599,6 +7615,13 @@ "albumName": { "type": "string" }, + "albumUsers": { + "description": "This property was added in v1.104.0", + "items": { + "$ref": "#/components/schemas/AlbumUserCreateDto" + }, + "type": "array" + }, "assetIds": { "items": { "format": "uuid", @@ -7610,6 +7633,8 @@ "type": "string" }, "sharedWithUserIds": { + "deprecated": true, + "description": "This property was deprecated in v1.104.0", "items": { "format": "uuid", "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 903d8e048d..23b3b00bed 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -169,10 +169,17 @@ export type AlbumResponseDto = { startDate?: string; updatedAt: string; }; +export type AlbumUserCreateDto = { + role: AlbumUserRole; + userId: string; +}; export type CreateAlbumDto = { albumName: string; + /** This property was added in v1.104.0 */ + albumUsers?: AlbumUserCreateDto[]; assetIds?: string[]; description?: string; + /** This property was deprecated in v1.104.0 */ sharedWithUserIds?: string[]; }; export type AlbumCountResponseDto = { diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index fb4aff9485..c588847c69 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; import { PropertyLifecycle } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; @@ -32,6 +33,14 @@ export class AddUsersDto { albumUsers!: AlbumUserAddDto[]; } +class AlbumUserCreateDto { + @ValidateUUID() + userId!: string; + + @IsEnum(AlbumUserRole) + role!: AlbumUserRole; +} + export class CreateAlbumDto { @IsString() @ApiProperty() @@ -41,7 +50,15 @@ export class CreateAlbumDto { @Optional() description?: string; + @Optional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AlbumUserCreateDto) + @PropertyLifecycle({ addedAt: 'v1.104.0' }) + albumUsers?: AlbumUserCreateDto[]; + @ValidateUUID({ optional: true, each: true }) + @PropertyLifecycle({ deprecatedAt: 'v1.104.0' }) sharedWithUserIds?: string[]; @ValidateUUID({ optional: true, each: true }) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 06891ef4dc..e3183c36aa 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -281,11 +281,11 @@ export class AlbumRepository implements IAlbumRepository { .execute(); } - async create(album: Partial): Promise { + create(album: Partial): Promise { return this.save(album); } - async update(album: Partial): Promise { + update(album: Partial): Promise { return this.save(album); } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 3a050cd594..e2a7fc49c4 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -194,7 +194,7 @@ describe(AlbumService.name, () => { ownerId: authStub.admin.user.id, albumName: albumStub.empty.albumName, description: albumStub.empty.description, - albumUsers: [{ user: { id: 'user-id' } }], + albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], assets: [{ id: '123' }], albumThumbnailAssetId: '123', }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 1cc049d851..38464bd75a 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -114,7 +114,12 @@ export class AlbumService { } async create(auth: AuthDto, dto: CreateAlbumDto): Promise { + const albumUsers = dto.albumUsers || []; for (const userId of dto.sharedWithUserIds || []) { + albumUsers.push({ userId, role: AlbumUserRole.EDITOR }); + } + + for (const { userId } of albumUsers) { const exists = await this.userRepository.get(userId, {}); if (!exists) { throw new BadRequestException('User not found'); @@ -128,7 +133,7 @@ export class AlbumService { ownerId: auth.user.id, albumName: dto.albumName, description: dto.description, - albumUsers: dto.sharedWithUserIds?.map((userId) => ({ user: { id: userId } }) as AlbumUserEntity) ?? [], + albumUsers: albumUsers.map((albumUser) => albumUser as AlbumUserEntity) ?? [], assets, albumThumbnailAssetId: assets[0]?.id || null, });