From e79d1b1ec269f316e101c4d75413d494c205df4d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 7 May 2024 16:38:09 -0400 Subject: [PATCH] refactor: create album users (#9315) --- e2e/src/api/specs/activity.e2e-spec.ts | 3 +- e2e/src/api/specs/album.e2e-spec.ts | 70 +++++++----------- mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | Bin 26794 -> 26846 bytes mobile/openapi/doc/AlbumUserCreateDto.md | Bin 0 -> 469 bytes mobile/openapi/doc/CreateAlbumDto.md | Bin 608 -> 792 bytes mobile/openapi/lib/api.dart | Bin 9537 -> 9578 bytes mobile/openapi/lib/api_client.dart | Bin 25860 -> 25948 bytes .../lib/model/album_user_create_dto.dart | Bin 0 -> 3031 bytes .../openapi/lib/model/create_album_dto.dart | Bin 4328 -> 4718 bytes .../test/album_user_create_dto_test.dart | Bin 0 -> 674 bytes .../openapi/test/create_album_dto_test.dart | Bin 959 -> 1199 bytes open-api/immich-openapi-specs.json | 25 +++++++ open-api/typescript-sdk/src/fetch-client.ts | 7 ++ server/src/dtos/album.dto.ts | 19 ++++- server/src/repositories/album.repository.ts | 4 +- server/src/services/album.service.spec.ts | 2 +- server/src/services/album.service.ts | 7 +- 18 files changed, 90 insertions(+), 50 deletions(-) create mode 100644 mobile/openapi/doc/AlbumUserCreateDto.md create mode 100644 mobile/openapi/lib/model/album_user_create_dto.dart create mode 100644 mobile/openapi/test/album_user_create_dto_test.dart diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index d3d7db697..a1b717883 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 a3459bea3..f7d05aac5 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 65cf5428f..570132ada 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 65ecb51b9b53926659475a1d920083c107cfdb5f..c98745430cab9c5d313c6e6b16937902cb633312 100644 GIT binary patch delta 35 ocmZ2=k@4O|#tnO1*qn<}6H8JjPjnGx1v4k#a}nRX*X6A;02Yo9HUIzs delta 14 Wcmcb2k#W^U#tnO1Hh*w=rwjl*q6gIg diff --git a/mobile/openapi/doc/AlbumUserCreateDto.md b/mobile/openapi/doc/AlbumUserCreateDto.md new file mode 100644 index 0000000000000000000000000000000000000000..78acbc476ba27d8c98b1e66b7f40c60fe62602eb GIT binary patch literal 469 zcma)2%WA_g5WMRv7JNu7kaKTOZkzGt1H_Qet$y7!hT@ZO~QwcxrdcXOnH!e}2Ro-b!R}o(We+@4gW1mUOknI2f literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/CreateAlbumDto.md b/mobile/openapi/doc/CreateAlbumDto.md index 0a472725e40838161e8fbd99deec369190ba5167..34035d4af67e965f9659fc4cf4d44b1feb4bc69b 100644 GIT binary patch delta 190 zcmaFBGJ|cx@A{mi(%jJE)S_Z7Erl9|Xe}+D%;FLoN0_j4QEFmIs!K_}ot9Rt2BxrH zZi*&QUr0t~u|h#nenDzcNu@%0VzEMEN=j;qLS~*qnW3Jcfr*|0$n^YzlFa!_?u zr1(FTQ0QK;Ej~-8@YisqF}Tvh?y)SiVTFlAf(wdNaAn=~;9|KbxQ=pJY|zYPtcZU7 zIf^r;^k{_38IUQ+k{7HL0sdc%Msd!xhRb|iZ9i$Q-YL!uzcLaYM*tIq;DMV;6&e^6 zdvsK}=r0 zv?<##b_*EW<^)GGE-RXQB0o_W*l+_G)7d+jaLmXCZWTvDD|pPcW@=ADLGClIg|=oM z!4Ndg=kSvob&GZ5C$Q9@8R=d;i^TYpu_eV1U(gd3ZW#$*xZl;-M!jR@8yCcfWRG$d z@eGW7uVsNz9pR}cSPq|Y2tqiqemq>*tif6=)tFWruh6vB$2SHpfzHHzJ0oR9YD3Y$ zft2$f#S5%Rz}2w^VKjVUdBruPN^a|DOFM*mPtjmZC9y;xRz4I}j(Q_;?S!^v&||Vx zJjH6Xg{-O=6{#cAYJ_!dL>yrY$Yesyx`BZb`psrx(XG*T*)>pOLY~cybc-o9;{Zn_ zSd$=T+MLkgxoUHTN(DcVg@-A+KjLcISv=rpKseb9iSd@t^D|fFHD&TarS@%~QpI-J?GI zr_i3$5gwIzBcPMtHR2;4aa8RaCIk8sTsQk6X55~h!>A1vSj!$iKVTNYS~|zS`JNi7 z^>^B&<-bEOy0*oH8^=#-ZWbr$PR^Y$84gh;(H-KHiC4}L{p!w%#{oLfb&BG3-R-V- zbdZ{cN@_+fG&!gdAD&mW7^APACl~ua8hWNYo+GuotM)p=50!*FJEr=SkASFZ;p+*? zZ;Q4oU%yqE6UkBS<6=E=Okk1mRtN2+4kf4YZQg%>02Gyeh*D$-s6 literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index 7b527eb7bcb75897ea8a7e9160ce66721a0ed767..7af3526a45eb4b57080c69c90e7ea61cb2cd4ba9 100644 GIT binary patch delta 336 zcmaE%_)cZRLPn9Cq|)5b;?$yI1zUyW{Ji24h3MGHQyHs$_4V}?LNYRo6$*;-3sQ?p zDiz8Tixm=6Qc_bCGV>J54D}2RO!N%66cl_ii%V=AVFo!Dr6!i7x|HPGDI~(giZ^pJ zO<~l*Ze&VoK~ZWlP!Ec^ljks>6@%;7K{88ovnz`*BPU#LvMcKuQ8>?51!3!CKDJx3 za2fSjpchMkKGH*|*nER+CX+o}iIoDTH}rCV-g7I;&-E(K&(p{P646EK2ouy}H8-bm HHn9T$+4Oim delta 42 zcmV+_0M-BQB_BbLt>{&96_TSi@CrOE7Y!k?p0MAa;*wke#nCrb>jr#Tce2OT2@?ZNqjpb%jRU; z<(bjeN$rHnIZ*8sQjpu$uu^HXqBOc-%fPXnH@;~iQ*#839ECd~+FY;2^KL7v# literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/create_album_dto_test.dart b/mobile/openapi/test/create_album_dto_test.dart index d23e66cf7ee0c227fd6e26d329cd683092e47258..f3dc3c8647845625cac3291e571e9f6a7cd5b1cd 100644 GIT binary patch delta 150 zcmdnbzMgY~DU)wVMrN@>K~a7|YEen0LV04bLSjlvYKlT;oo&lEv5a{bG z_+%EB*f{1SmF9*PrxrOEr6!i7x|HPGDI~(gCVMj3PGVwV0m)D9V^*K6$MjtT(}tAP Rf}+%9pl%dvCd)D}0sye6G1UM7 delta 15 XcmZ3_xu1Q5DbwVyOdlqTvdjViF}(&R diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ae6a3dfb0..993705f97 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 903d8e048..23b3b00be 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 fb4aff948..c588847c6 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 06891ef4d..e3183c36a 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 3a050cd59..e2a7fc49c 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 1cc049d85..38464bd75 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, });