mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-28 01:02:33 -04:00 
			
		
		
		
	feat: readonly album sharing (#8720)
* rename albums_shared_users_users to album_permissions and add readonly column * disable synchronize on the original join table * remove unnecessary FK names * set readonly=true as default for new album shares * separate and implement album READ and WRITE permission * expose albumPermissions on the API, deprecate sharedUsers * generate openapi * create readonly view on frontend * ??? move slideshow button out from ellipsis menu so that non-owners can have access too * correct sharedUsers joins * add album permission repository * remove a log * fix assetCount getting reset when adding users * fix lint * add set permission endpoint and UI * sort users * remove log * Revert "??? move slideshow button out from ellipsis menu so that non-owners can have access too" This reverts commit 1343bfa31125f7136f81db28f7aa4c5ef0204847. * rename stuff * fix db schema annotations * sql generate * change readonly default to follow migration * fix deprecation notice * change readonly boolean to role enum * fix joincolumn as primary key * rename albumUserRepository in album service * clean up userId and albumId * add write access to shared link * fix existing tests * switch to vitest * format and fix tests on web * add new test * fix one e2e test * rename new API field to albumUsers * capitalize serverside enum * remove unused ReadWrite type * missed rename from previous commit * rename to albumUsers in album entity as well * remove outdated Equals calls * unnecessary relation * rename to updateUser in album service * minor renamery * move sorting to backend * rename and separate ALBUM_WRITE as ADD_ASSET and REMOVE_ASSET * fix tests * fix "should migrate single moving picture" test failing on European system timezone * generated changes after merge * lint fix * fix correct page to open after removing user from album * fix e2e tests and some bugs * rename updateAlbumUser rest endpoint * add new e2e tests for updateAlbumUser endpoint * small optimizations * refactor album e2e test, add new album shared with viewer * add new test to check if viewer can see the album * add new e2e tests for readonly share * failing test: User delete doesn't cascade to UserAlbum entity * fix: handle deleted users * use lodash for sort * add role to addUsersToAlbum endpoint * add UI for adding editors * lint fixes * change role back to editor as DB default * fix server tests * redesign user selection modal editor selector * style tweaks * fix type error * Revert "style tweaks" This reverts commit ab604f4c8f3a6f12ab0b5fe2dd2ede723aa68775. * Revert "redesign user selection modal editor selector" This reverts commit e6f344856c6c05e4eb5c78f0dffb9f52498795f4. * chore: cleanup and improve add user modal * chore: open api * small styling --------- Co-authored-by: mgabor <> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									0b3373c552
								
							
						
					
					
						commit
						2943f93098
					
				| @ -1,12 +1,13 @@ | ||||
| import { | ||||
|   addAssetsToAlbum, | ||||
|   AlbumResponseDto, | ||||
|   AlbumUserRole, | ||||
|   AssetFileUploadResponseDto, | ||||
|   AssetOrder, | ||||
|   LoginResponseDto, | ||||
|   SharedLinkType, | ||||
|   addAssetsToAlbum, | ||||
|   deleteUser, | ||||
|   getAlbumInfo, | ||||
|   LoginResponseDto, | ||||
|   SharedLinkType, | ||||
| } from '@immich/sdk'; | ||||
| import { createUserDto, uuidDto } from 'src/fixtures'; | ||||
| import { errorDto } from 'src/responses'; | ||||
| @ -14,7 +15,8 @@ import { app, asBearerAuth, utils } from 'src/utils'; | ||||
| import request from 'supertest'; | ||||
| import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; | ||||
| 
 | ||||
| const user1SharedUser = 'user1SharedUser'; | ||||
| const user1SharedEditorUser = 'user1SharedEditorUser'; | ||||
| const user1SharedViewerUser = 'user1SharedViewerUser'; | ||||
| const user1SharedLink = 'user1SharedLink'; | ||||
| const user1NotShared = 'user1NotShared'; | ||||
| const user2SharedUser = 'user2SharedUser'; | ||||
| @ -49,35 +51,61 @@ describe('/album', () => { | ||||
| 
 | ||||
|     const albums = await Promise.all([ | ||||
|       // user 1
 | ||||
|       /* 0 */ | ||||
|       utils.createAlbum(user1.accessToken, { | ||||
|         albumName: user1SharedUser, | ||||
|         albumName: user1SharedEditorUser, | ||||
|         sharedWithUserIds: [user2.userId], | ||||
|         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], | ||||
|         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], | ||||
|         assetIds: [user1Asset1.id], | ||||
|       }), | ||||
|     ]); | ||||
| 
 | ||||
|     // Make viewer
 | ||||
|     await utils.updateAlbumUser(user1.accessToken, { | ||||
|       id: albums[7].id, | ||||
|       userId: user2.userId, | ||||
|       updateAlbumUserDto: { role: AlbumUserRole.Viewer }, | ||||
|     }); | ||||
| 
 | ||||
|     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] } }, | ||||
|       { headers: asBearerAuth(user1.accessToken) }, | ||||
| @ -85,7 +113,7 @@ describe('/album', () => { | ||||
| 
 | ||||
|     albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) }); | ||||
| 
 | ||||
|     user1Albums = albums.slice(0, 3); | ||||
|     user1Albums = [...albums.slice(0, 3), albums[7]]; | ||||
|     user2Albums = albums.slice(3, 6); | ||||
| 
 | ||||
|     await Promise.all([ | ||||
| @ -144,7 +172,7 @@ describe('/album', () => { | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(3); | ||||
|       expect(body).toHaveLength(4); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ | ||||
| @ -154,7 +182,12 @@ describe('/album', () => { | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedUser, | ||||
|             albumName: user1SharedEditorUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedViewerUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
| @ -169,12 +202,17 @@ describe('/album', () => { | ||||
|     it('should return the album collection including owned and shared', async () => { | ||||
|       const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(3); | ||||
|       expect(body).toHaveLength(4); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedUser, | ||||
|             albumName: user1SharedEditorUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedViewerUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
| @ -196,12 +234,17 @@ describe('/album', () => { | ||||
|         .get('/album?shared=true') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(3); | ||||
|       expect(body).toHaveLength(4); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedUser, | ||||
|             albumName: user1SharedEditorUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
|             ownerId: user1.userId, | ||||
|             albumName: user1SharedViewerUser, | ||||
|             shared: true, | ||||
|           }), | ||||
|           expect.objectContaining({ | ||||
| @ -248,7 +291,7 @@ describe('/album', () => { | ||||
|         .get(`/album?shared=true&assetId=${user1Asset1.id}`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(4); | ||||
|       expect(body).toHaveLength(5); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the album collection filtered by assetId and ignores shared=false', async () => { | ||||
| @ -256,7 +299,7 @@ describe('/album', () => { | ||||
|         .get(`/album?shared=false&assetId=${user1Asset1.id}`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(4); | ||||
|       expect(body).toHaveLength(5); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -279,16 +322,22 @@ describe('/album', () => { | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return album info for shared album', async () => { | ||||
|     it('should return album info for shared album (editor)', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get(`/album/${user2Albums[0].id}?withoutAssets=false`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ | ||||
|         ...user2Albums[0], | ||||
|         assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })], | ||||
|       }); | ||||
|       expect(body).toMatchObject({ id: user2Albums[0].id }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return album info for shared album (viewer)', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get(`/album/${user1Albums[3].id}?withoutAssets=false`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id: user1Albums[3].id }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return album info with assets when withoutAssets is undefined', async () => { | ||||
| @ -330,7 +379,7 @@ describe('/album', () => { | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 }); | ||||
|       expect(body).toEqual({ owned: 4, shared: 4, notShared: 1 }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -357,6 +406,7 @@ describe('/album', () => { | ||||
|         albumThumbnailAssetId: null, | ||||
|         shared: false, | ||||
|         sharedUsers: [], | ||||
|         albumUsers: [], | ||||
|         hasSharedLink: false, | ||||
|         assets: [], | ||||
|         assetCount: 0, | ||||
| @ -395,6 +445,17 @@ describe('/album', () => { | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not be able to add assets to album as a viewer', async () => { | ||||
|       const asset = await utils.createAsset(user2.accessToken); | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/album/${user1Albums[3].id}/assets`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`) | ||||
|         .send({ ids: [asset.id] }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access')); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('PATCH /album/:id', () => { | ||||
| @ -425,6 +486,26 @@ describe('/album', () => { | ||||
|         description: 'An album description', | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not be able to update as a viewer', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .patch(`/album/${user1Albums[3].id}`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`) | ||||
|         .send({ albumName: 'New album name' }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Not found or no album.update access')); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not be able to update as an editor', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .patch(`/album/${user1Albums[0].id}`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`) | ||||
|         .send({ albumName: 'New album name' }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Not found or no album.update access')); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('DELETE /album/:id/assets', () => { | ||||
| @ -488,6 +569,16 @@ describe('/album', () => { | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not be able to remove assets from album as a viewer', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .delete(`/album/${user1Albums[3].id}/assets`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`) | ||||
|         .send({ ids: [user1Asset1.id] }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access')); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('PUT :id/users', () => { | ||||
| @ -510,7 +601,7 @@ describe('/album', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/album/${album.id}/users`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ sharedUserIds: [user2.userId] }); | ||||
|         .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual( | ||||
| @ -524,7 +615,7 @@ describe('/album', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/album/${album.id}/users`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ sharedUserIds: [user1.userId] }); | ||||
|         .send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner')); | ||||
| @ -534,15 +625,54 @@ describe('/album', () => { | ||||
|       await request(app) | ||||
|         .put(`/album/${album.id}/users`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ sharedUserIds: [user2.userId] }); | ||||
|         .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); | ||||
| 
 | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/album/${album.id}/users`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ sharedUserIds: [user2.userId] }); | ||||
|         .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('User already added')); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('PUT :id/user/:userId', () => { | ||||
|     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], | ||||
|       }); | ||||
| 
 | ||||
|       const { status } = await request(app) | ||||
|         .put(`/album/${album.id}/user/${user2.userId}`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ role: AlbumUserRole.Editor }); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
| 
 | ||||
|       // Get album to verify the role change
 | ||||
|       const { body } = await request(app).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(body).toEqual( | ||||
|         expect.objectContaining({ | ||||
|           albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })], | ||||
|         }), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     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], | ||||
|       }); | ||||
| 
 | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/album/${album.id}/user/${user2.userId}`) | ||||
|         .set('Authorization', `Bearer ${user2.accessToken}`) | ||||
|         .send({ role: AlbumUserRole.Editor }); | ||||
| 
 | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest('Not found or no album.share access')); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -26,6 +26,7 @@ import { | ||||
|   searchMetadata, | ||||
|   signUpAdmin, | ||||
|   updateAdminOnboarding, | ||||
|   updateAlbumUser, | ||||
|   updateConfig, | ||||
|   validate, | ||||
| } from '@immich/sdk'; | ||||
| @ -286,6 +287,9 @@ export const utils = { | ||||
|   createAlbum: (accessToken: string, dto: CreateAlbumDto) => | ||||
|     createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }), | ||||
| 
 | ||||
|   updateAlbumUser: (accessToken: string, args: Parameters<typeof updateAlbumUser>[0]) => | ||||
|     updateAlbumUser(args, { headers: asBearerAuth(accessToken) }), | ||||
| 
 | ||||
|   createAsset: async ( | ||||
|     accessToken: string, | ||||
|     dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData }, | ||||
|  | ||||
							
								
								
									
										12
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @ -17,6 +17,9 @@ doc/AdminOnboardingUpdateDto.md | ||||
| doc/AlbumApi.md | ||||
| doc/AlbumCountResponseDto.md | ||||
| doc/AlbumResponseDto.md | ||||
| doc/AlbumUserAddDto.md | ||||
| doc/AlbumUserResponseDto.md | ||||
| doc/AlbumUserRole.md | ||||
| doc/AllJobStatusResponseDto.md | ||||
| doc/AssetApi.md | ||||
| doc/AssetBulkDeleteDto.md | ||||
| @ -189,6 +192,7 @@ doc/TranscodeHWAccel.md | ||||
| doc/TranscodePolicy.md | ||||
| doc/TrashApi.md | ||||
| doc/UpdateAlbumDto.md | ||||
| doc/UpdateAlbumUserDto.md | ||||
| doc/UpdateAssetDto.md | ||||
| doc/UpdateLibraryDto.md | ||||
| doc/UpdatePartnerDto.md | ||||
| @ -249,6 +253,9 @@ lib/model/add_users_dto.dart | ||||
| 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_response_dto.dart | ||||
| lib/model/album_user_role.dart | ||||
| lib/model/all_job_status_response_dto.dart | ||||
| lib/model/api_key_create_dto.dart | ||||
| lib/model/api_key_create_response_dto.dart | ||||
| @ -403,6 +410,7 @@ lib/model/tone_mapping.dart | ||||
| lib/model/transcode_hw_accel.dart | ||||
| lib/model/transcode_policy.dart | ||||
| lib/model/update_album_dto.dart | ||||
| lib/model/update_album_user_dto.dart | ||||
| lib/model/update_asset_dto.dart | ||||
| lib/model/update_library_dto.dart | ||||
| lib/model/update_partner_dto.dart | ||||
| @ -429,6 +437,9 @@ test/admin_onboarding_update_dto_test.dart | ||||
| 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_response_dto_test.dart | ||||
| test/album_user_role_test.dart | ||||
| test/all_job_status_response_dto_test.dart | ||||
| test/api_key_api_test.dart | ||||
| test/api_key_create_dto_test.dart | ||||
| @ -606,6 +617,7 @@ test/transcode_hw_accel_test.dart | ||||
| test/transcode_policy_test.dart | ||||
| test/trash_api_test.dart | ||||
| test/update_album_dto_test.dart | ||||
| test/update_album_user_dto_test.dart | ||||
| test/update_asset_dto_test.dart | ||||
| test/update_library_dto_test.dart | ||||
| test/update_partner_dto_test.dart | ||||
|  | ||||
							
								
								
									
										5
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @ -91,6 +91,7 @@ Class | Method | HTTP request | Description | ||||
| *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |  | ||||
| *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |  | ||||
| *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |  | ||||
| *AlbumApi* | [**updateAlbumUser**](doc//AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/user/{userId} |  | ||||
| *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check |  | ||||
| *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset |  | ||||
| @ -238,6 +239,9 @@ Class | Method | HTTP request | Description | ||||
|  - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) | ||||
|  - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) | ||||
|  - [AlbumResponseDto](doc//AlbumResponseDto.md) | ||||
|  - [AlbumUserAddDto](doc//AlbumUserAddDto.md) | ||||
|  - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) | ||||
|  - [AlbumUserRole](doc//AlbumUserRole.md) | ||||
|  - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) | ||||
|  - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) | ||||
|  - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) | ||||
| @ -388,6 +392,7 @@ Class | Method | HTTP request | Description | ||||
|  - [TranscodeHWAccel](doc//TranscodeHWAccel.md) | ||||
|  - [TranscodePolicy](doc//TranscodePolicy.md) | ||||
|  - [UpdateAlbumDto](doc//UpdateAlbumDto.md) | ||||
|  - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) | ||||
|  - [UpdateAssetDto](doc//UpdateAssetDto.md) | ||||
|  - [UpdateLibraryDto](doc//UpdateLibraryDto.md) | ||||
|  - [UpdatePartnerDto](doc//UpdatePartnerDto.md) | ||||
|  | ||||
							
								
								
									
										3
									
								
								mobile/openapi/doc/AddUsersDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/doc/AddUsersDto.md
									
									
									
										generated
									
									
									
								
							| @ -8,7 +8,8 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **sharedUserIds** | **List<String>** |  | [default to const []] | ||||
| **albumUsers** | [**List<AlbumUserAddDto>**](AlbumUserAddDto.md) |  | [default to const []] | ||||
| **sharedUserIds** | **List<String>** | Deprecated in favor of albumUsers | [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) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										59
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							| @ -19,6 +19,7 @@ Method | HTTP request | Description | ||||
| [**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |  | ||||
| [**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |  | ||||
| [**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |  | ||||
| [**updateAlbumUser**](AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/user/{userId} |  | ||||
| 
 | ||||
| 
 | ||||
| # **addAssetsToAlbum** | ||||
| @ -583,3 +584,61 @@ Name | Type | Description  | Notes | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **updateAlbumUser** | ||||
| > updateAlbumUser(id, userId, updateAlbumUserDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure API key authorization: cookie | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; | ||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; | ||||
| // TODO Configure API key authorization: api_key | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; | ||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AlbumApi(); | ||||
| final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final userId = userId_example; // String |  | ||||
| final updateAlbumUserDto = UpdateAlbumUserDto(); // UpdateAlbumUserDto |  | ||||
| 
 | ||||
| try { | ||||
|     api_instance.updateAlbumUser(id, userId, updateAlbumUserDto); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AlbumApi->updateAlbumUser: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **id** | **String**|  |  | ||||
|  **userId** | **String**|  |  | ||||
|  **updateAlbumUserDto** | [**UpdateAlbumUserDto**](UpdateAlbumUserDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| void (empty response body) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: Not defined | ||||
| 
 | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								mobile/openapi/doc/AlbumResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/doc/AlbumResponseDto.md
									
									
									
										generated
									
									
									
								
							| @ -10,6 +10,7 @@ Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **albumName** | **String** |  |  | ||||
| **albumThumbnailAssetId** | **String** |  |  | ||||
| **albumUsers** | [**List<AlbumUserResponseDto>**](AlbumUserResponseDto.md) |  | [default to const []] | ||||
| **assetCount** | **int** |  |  | ||||
| **assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []] | ||||
| **createdAt** | [**DateTime**](DateTime.md) |  |  | ||||
| @ -23,7 +24,7 @@ Name | Type | Description | Notes | ||||
| **owner** | [**UserResponseDto**](UserResponseDto.md) |  |  | ||||
| **ownerId** | **String** |  |  | ||||
| **shared** | **bool** |  |  | ||||
| **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) |  | [default to const []] | ||||
| **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of albumUsers | [default to const []] | ||||
| **startDate** | [**DateTime**](DateTime.md) |  | [optional]  | ||||
| **updatedAt** | [**DateTime**](DateTime.md) |  |  | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/AlbumUserAddDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/AlbumUserAddDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # openapi.model.AlbumUserAddDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **role** | [**AlbumUserRole**](AlbumUserRole.md) |  | [optional]  | ||||
| **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) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/AlbumUserResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/AlbumUserResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # openapi.model.AlbumUserResponseDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **role** | [**AlbumUserRole**](AlbumUserRole.md) |  |  | ||||
| **user** | [**UserResponseDto**](UserResponseDto.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) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										14
									
								
								mobile/openapi/doc/AlbumUserRole.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/doc/AlbumUserRole.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| # openapi.model.AlbumUserRole | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/UpdateAlbumUserDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/UpdateAlbumUserDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| # openapi.model.UpdateAlbumUserDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **role** | [**AlbumUserRole**](AlbumUserRole.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) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @ -67,6 +67,9 @@ part 'model/add_users_dto.dart'; | ||||
| 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_response_dto.dart'; | ||||
| part 'model/album_user_role.dart'; | ||||
| part 'model/all_job_status_response_dto.dart'; | ||||
| part 'model/asset_bulk_delete_dto.dart'; | ||||
| part 'model/asset_bulk_update_dto.dart'; | ||||
| @ -217,6 +220,7 @@ part 'model/tone_mapping.dart'; | ||||
| part 'model/transcode_hw_accel.dart'; | ||||
| part 'model/transcode_policy.dart'; | ||||
| part 'model/update_album_dto.dart'; | ||||
| part 'model/update_album_user_dto.dart'; | ||||
| part 'model/update_asset_dto.dart'; | ||||
| part 'model/update_library_dto.dart'; | ||||
| part 'model/update_partner_dto.dart'; | ||||
|  | ||||
							
								
								
									
										49
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										49
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							| @ -536,4 +536,53 @@ class AlbumApi { | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'PUT /album/{id}/user/{userId}' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [String] userId (required): | ||||
|   /// | ||||
|   /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): | ||||
|   Future<Response> updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/album/{id}/user/{userId}' | ||||
|       .replaceAll('{id}', id) | ||||
|       .replaceAll('{userId}', userId); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = updateAlbumUserDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'PUT', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [String] userId (required): | ||||
|   /// | ||||
|   /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): | ||||
|   Future<void> updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { | ||||
|     final response = await updateAlbumUserWithHttpInfo(id, userId, updateAlbumUserDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										8
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @ -204,6 +204,12 @@ class ApiClient { | ||||
|           return AlbumCountResponseDto.fromJson(value); | ||||
|         case 'AlbumResponseDto': | ||||
|           return AlbumResponseDto.fromJson(value); | ||||
|         case 'AlbumUserAddDto': | ||||
|           return AlbumUserAddDto.fromJson(value); | ||||
|         case 'AlbumUserResponseDto': | ||||
|           return AlbumUserResponseDto.fromJson(value); | ||||
|         case 'AlbumUserRole': | ||||
|           return AlbumUserRoleTypeTransformer().decode(value); | ||||
|         case 'AllJobStatusResponseDto': | ||||
|           return AllJobStatusResponseDto.fromJson(value); | ||||
|         case 'AssetBulkDeleteDto': | ||||
| @ -504,6 +510,8 @@ class ApiClient { | ||||
|           return TranscodePolicyTypeTransformer().decode(value); | ||||
|         case 'UpdateAlbumDto': | ||||
|           return UpdateAlbumDto.fromJson(value); | ||||
|         case 'UpdateAlbumUserDto': | ||||
|           return UpdateAlbumUserDto.fromJson(value); | ||||
|         case 'UpdateAssetDto': | ||||
|           return UpdateAssetDto.fromJson(value); | ||||
|         case 'UpdateLibraryDto': | ||||
|  | ||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							| @ -55,6 +55,9 @@ String parameterToString(dynamic value) { | ||||
|   if (value is DateTime) { | ||||
|     return value.toUtc().toIso8601String(); | ||||
|   } | ||||
|   if (value is AlbumUserRole) { | ||||
|     return AlbumUserRoleTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   if (value is AssetJobName) { | ||||
|     return AssetJobNameTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|  | ||||
							
								
								
									
										12
									
								
								mobile/openapi/lib/model/add_users_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/lib/model/add_users_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -13,25 +13,32 @@ part of openapi.api; | ||||
| class AddUsersDto { | ||||
|   /// Returns a new [AddUsersDto] instance. | ||||
|   AddUsersDto({ | ||||
|     this.albumUsers = const [], | ||||
|     this.sharedUserIds = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<AlbumUserAddDto> albumUsers; | ||||
| 
 | ||||
|   /// Deprecated in favor of albumUsers | ||||
|   List<String> sharedUserIds; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AddUsersDto && | ||||
|     _deepEquality.equals(other.albumUsers, albumUsers) && | ||||
|     _deepEquality.equals(other.sharedUserIds, sharedUserIds); | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (albumUsers.hashCode) + | ||||
|     (sharedUserIds.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AddUsersDto[sharedUserIds=$sharedUserIds]'; | ||||
|   String toString() => 'AddUsersDto[albumUsers=$albumUsers, sharedUserIds=$sharedUserIds]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'albumUsers'] = this.albumUsers; | ||||
|       json[r'sharedUserIds'] = this.sharedUserIds; | ||||
|     return json; | ||||
|   } | ||||
| @ -44,6 +51,7 @@ class AddUsersDto { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AddUsersDto( | ||||
|         albumUsers: AlbumUserAddDto.listFromJson(json[r'albumUsers']), | ||||
|         sharedUserIds: json[r'sharedUserIds'] is Iterable | ||||
|             ? (json[r'sharedUserIds'] as Iterable).cast<String>().toList(growable: false) | ||||
|             : const [], | ||||
| @ -94,7 +102,7 @@ class AddUsersDto { | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'sharedUserIds', | ||||
|     'albumUsers', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										11
									
								
								mobile/openapi/lib/model/album_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								mobile/openapi/lib/model/album_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -15,6 +15,7 @@ class AlbumResponseDto { | ||||
|   AlbumResponseDto({ | ||||
|     required this.albumName, | ||||
|     required this.albumThumbnailAssetId, | ||||
|     this.albumUsers = const [], | ||||
|     required this.assetCount, | ||||
|     this.assets = const [], | ||||
|     required this.createdAt, | ||||
| @ -37,6 +38,8 @@ class AlbumResponseDto { | ||||
| 
 | ||||
|   String? albumThumbnailAssetId; | ||||
| 
 | ||||
|   List<AlbumUserResponseDto> albumUsers; | ||||
| 
 | ||||
|   int assetCount; | ||||
| 
 | ||||
|   List<AssetResponseDto> assets; | ||||
| @ -81,6 +84,7 @@ class AlbumResponseDto { | ||||
| 
 | ||||
|   bool shared; | ||||
| 
 | ||||
|   /// Deprecated in favor of albumUsers | ||||
|   List<UserResponseDto> sharedUsers; | ||||
| 
 | ||||
|   /// | ||||
| @ -97,6 +101,7 @@ class AlbumResponseDto { | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && | ||||
|     other.albumName == albumName && | ||||
|     other.albumThumbnailAssetId == albumThumbnailAssetId && | ||||
|     _deepEquality.equals(other.albumUsers, albumUsers) && | ||||
|     other.assetCount == assetCount && | ||||
|     _deepEquality.equals(other.assets, assets) && | ||||
|     other.createdAt == createdAt && | ||||
| @ -119,6 +124,7 @@ class AlbumResponseDto { | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (albumName.hashCode) + | ||||
|     (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + | ||||
|     (albumUsers.hashCode) + | ||||
|     (assetCount.hashCode) + | ||||
|     (assets.hashCode) + | ||||
|     (createdAt.hashCode) + | ||||
| @ -137,7 +143,7 @@ class AlbumResponseDto { | ||||
|     (updatedAt.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; | ||||
|   String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @ -147,6 +153,7 @@ class AlbumResponseDto { | ||||
|     } else { | ||||
|     //  json[r'albumThumbnailAssetId'] = null; | ||||
|     } | ||||
|       json[r'albumUsers'] = this.albumUsers; | ||||
|       json[r'assetCount'] = this.assetCount; | ||||
|       json[r'assets'] = this.assets; | ||||
|       json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); | ||||
| @ -192,6 +199,7 @@ class AlbumResponseDto { | ||||
|       return AlbumResponseDto( | ||||
|         albumName: mapValueOfType<String>(json, r'albumName')!, | ||||
|         albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'), | ||||
|         albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), | ||||
|         assetCount: mapValueOfType<int>(json, r'assetCount')!, | ||||
|         assets: AssetResponseDto.listFromJson(json[r'assets']), | ||||
|         createdAt: mapDateTime(json, r'createdAt', r'')!, | ||||
| @ -257,6 +265,7 @@ class AlbumResponseDto { | ||||
|   static const requiredKeys = <String>{ | ||||
|     'albumName', | ||||
|     'albumThumbnailAssetId', | ||||
|     'albumUsers', | ||||
|     'assetCount', | ||||
|     'assets', | ||||
|     'createdAt', | ||||
|  | ||||
							
								
								
									
										115
									
								
								mobile/openapi/lib/model/album_user_add_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/openapi/lib/model/album_user_add_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | ||||
| // | ||||
| // 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 AlbumUserAddDto { | ||||
|   /// Returns a new [AlbumUserAddDto] instance. | ||||
|   AlbumUserAddDto({ | ||||
|     this.role, | ||||
|     required this.userId, | ||||
|   }); | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AlbumUserRole? role; | ||||
| 
 | ||||
|   String userId; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AlbumUserAddDto && | ||||
|     other.role == role && | ||||
|     other.userId == userId; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (role == null ? 0 : role!.hashCode) + | ||||
|     (userId.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AlbumUserAddDto[role=$role, userId=$userId]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|     if (this.role != null) { | ||||
|       json[r'role'] = this.role; | ||||
|     } else { | ||||
|     //  json[r'role'] = null; | ||||
|     } | ||||
|       json[r'userId'] = this.userId; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AlbumUserAddDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AlbumUserAddDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AlbumUserAddDto( | ||||
|         role: AlbumUserRole.fromJson(json[r'role']), | ||||
|         userId: mapValueOfType<String>(json, r'userId')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AlbumUserAddDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AlbumUserAddDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AlbumUserAddDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AlbumUserAddDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AlbumUserAddDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AlbumUserAddDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AlbumUserAddDto-objects as value to a dart map | ||||
|   static Map<String, List<AlbumUserAddDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AlbumUserAddDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AlbumUserAddDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'userId', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										106
									
								
								mobile/openapi/lib/model/album_user_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								mobile/openapi/lib/model/album_user_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -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 AlbumUserResponseDto { | ||||
|   /// Returns a new [AlbumUserResponseDto] instance. | ||||
|   AlbumUserResponseDto({ | ||||
|     required this.role, | ||||
|     required this.user, | ||||
|   }); | ||||
| 
 | ||||
|   AlbumUserRole role; | ||||
| 
 | ||||
|   UserResponseDto user; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AlbumUserResponseDto && | ||||
|     other.role == role && | ||||
|     other.user == user; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (role.hashCode) + | ||||
|     (user.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AlbumUserResponseDto[role=$role, user=$user]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'role'] = this.role; | ||||
|       json[r'user'] = this.user; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AlbumUserResponseDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AlbumUserResponseDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AlbumUserResponseDto( | ||||
|         role: AlbumUserRole.fromJson(json[r'role'])!, | ||||
|         user: UserResponseDto.fromJson(json[r'user'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AlbumUserResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AlbumUserResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AlbumUserResponseDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AlbumUserResponseDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AlbumUserResponseDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AlbumUserResponseDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AlbumUserResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<AlbumUserResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AlbumUserResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AlbumUserResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'role', | ||||
|     'user', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										85
									
								
								mobile/openapi/lib/model/album_user_role.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								mobile/openapi/lib/model/album_user_role.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| // | ||||
| // 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 AlbumUserRole { | ||||
|   /// Instantiate a new enum with the provided [value]. | ||||
|   const AlbumUserRole._(this.value); | ||||
| 
 | ||||
|   /// The underlying value of this enum member. | ||||
|   final String value; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => value; | ||||
| 
 | ||||
|   String toJson() => value; | ||||
| 
 | ||||
|   static const editor = AlbumUserRole._(r'editor'); | ||||
|   static const viewer = AlbumUserRole._(r'viewer'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][AlbumUserRole]. | ||||
|   static const values = <AlbumUserRole>[ | ||||
|     editor, | ||||
|     viewer, | ||||
|   ]; | ||||
| 
 | ||||
|   static AlbumUserRole? fromJson(dynamic value) => AlbumUserRoleTypeTransformer().decode(value); | ||||
| 
 | ||||
|   static List<AlbumUserRole> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AlbumUserRole>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AlbumUserRole.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Transformation class that can [encode] an instance of [AlbumUserRole] to String, | ||||
| /// and [decode] dynamic data back to [AlbumUserRole]. | ||||
| class AlbumUserRoleTypeTransformer { | ||||
|   factory AlbumUserRoleTypeTransformer() => _instance ??= const AlbumUserRoleTypeTransformer._(); | ||||
| 
 | ||||
|   const AlbumUserRoleTypeTransformer._(); | ||||
| 
 | ||||
|   String encode(AlbumUserRole data) => data.value; | ||||
| 
 | ||||
|   /// Decodes a [dynamic value][data] to a AlbumUserRole. | ||||
|   /// | ||||
|   /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, | ||||
|   /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] | ||||
|   /// cannot be decoded successfully, then an [UnimplementedError] is thrown. | ||||
|   /// | ||||
|   /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, | ||||
|   /// and users are still using an old app with the old code. | ||||
|   AlbumUserRole? decode(dynamic data, {bool allowNull = true}) { | ||||
|     if (data != null) { | ||||
|       switch (data) { | ||||
|         case r'editor': return AlbumUserRole.editor; | ||||
|         case r'viewer': return AlbumUserRole.viewer; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|           } | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Singleton [AlbumUserRoleTypeTransformer] instance. | ||||
|   static AlbumUserRoleTypeTransformer? _instance; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										98
									
								
								mobile/openapi/lib/model/update_album_user_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								mobile/openapi/lib/model/update_album_user_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| // | ||||
| // 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 UpdateAlbumUserDto { | ||||
|   /// Returns a new [UpdateAlbumUserDto] instance. | ||||
|   UpdateAlbumUserDto({ | ||||
|     required this.role, | ||||
|   }); | ||||
| 
 | ||||
|   AlbumUserRole role; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto && | ||||
|     other.role == role; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (role.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'UpdateAlbumUserDto[role=$role]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'role'] = this.role; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [UpdateAlbumUserDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static UpdateAlbumUserDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return UpdateAlbumUserDto( | ||||
|         role: AlbumUserRole.fromJson(json[r'role'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<UpdateAlbumUserDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <UpdateAlbumUserDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = UpdateAlbumUserDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, UpdateAlbumUserDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, UpdateAlbumUserDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = UpdateAlbumUserDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of UpdateAlbumUserDto-objects as value to a dart map | ||||
|   static Map<String, List<UpdateAlbumUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<UpdateAlbumUserDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = UpdateAlbumUserDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'role', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										6
									
								
								mobile/openapi/test/add_users_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/test/add_users_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @ -16,6 +16,12 @@ void main() { | ||||
|   // final instance = AddUsersDto(); | ||||
| 
 | ||||
|   group('test AddUsersDto', () { | ||||
|     // List<AlbumUserAddDto> albumUsers (default value: const []) | ||||
|     test('to test the property `albumUsers`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Deprecated in favor of albumUsers | ||||
|     // List<String> sharedUserIds (default value: const []) | ||||
|     test('to test the property `sharedUserIds`', () async { | ||||
|       // TODO | ||||
|  | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							| @ -67,5 +67,10 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto) async | ||||
|     test('test updateAlbumUser', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|   }); | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								mobile/openapi/test/album_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/test/album_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @ -26,6 +26,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<AlbumUserResponseDto> albumUsers (default value: const []) | ||||
|     test('to test the property `albumUsers`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // int assetCount | ||||
|     test('to test the property `assetCount`', () async { | ||||
|       // TODO | ||||
| @ -91,6 +96,7 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Deprecated in favor of albumUsers | ||||
|     // List<UserResponseDto> sharedUsers (default value: const []) | ||||
|     test('to test the property `sharedUsers`', () async { | ||||
|       // TODO | ||||
|  | ||||
							
								
								
									
										32
									
								
								mobile/openapi/test/album_user_add_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/album_user_add_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -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 AlbumUserAddDto | ||||
| void main() { | ||||
|   // final instance = AlbumUserAddDto(); | ||||
| 
 | ||||
|   group('test AlbumUserAddDto', () { | ||||
|     // AlbumUserRole role | ||||
|     test('to test the property `role`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // String userId | ||||
|     test('to test the property `userId`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										32
									
								
								mobile/openapi/test/album_user_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/album_user_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -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 AlbumUserResponseDto | ||||
| void main() { | ||||
|   // final instance = AlbumUserResponseDto(); | ||||
| 
 | ||||
|   group('test AlbumUserResponseDto', () { | ||||
|     // AlbumUserRole role | ||||
|     test('to test the property `role`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // UserResponseDto user | ||||
|     test('to test the property `user`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										21
									
								
								mobile/openapi/test/album_user_role_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/test/album_user_role_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| // | ||||
| // 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 AlbumUserRole | ||||
| void main() { | ||||
| 
 | ||||
|   group('test AlbumUserRole', () { | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										27
									
								
								mobile/openapi/test/update_album_user_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								mobile/openapi/test/update_album_user_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| // | ||||
| // 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 UpdateAlbumUserDto | ||||
| void main() { | ||||
|   // final instance = UpdateAlbumUserDto(); | ||||
| 
 | ||||
|   group('test UpdateAlbumUserDto', () { | ||||
|     // AlbumUserRole role | ||||
|     test('to test the property `role`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @ -630,6 +630,57 @@ | ||||
|         "tags": [ | ||||
|           "Album" | ||||
|         ] | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateAlbumUser", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "id", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "userId", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "requestBody": { | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/UpdateAlbumUserDto" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "required": true | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Album" | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/album/{id}/users": { | ||||
| @ -7251,7 +7302,15 @@ | ||||
|       }, | ||||
|       "AddUsersDto": { | ||||
|         "properties": { | ||||
|           "albumUsers": { | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/AlbumUserAddDto" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "sharedUserIds": { | ||||
|             "deprecated": true, | ||||
|             "description": "Deprecated in favor of albumUsers", | ||||
|             "items": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
| @ -7260,7 +7319,7 @@ | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "sharedUserIds" | ||||
|           "albumUsers" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
| @ -7303,6 +7362,12 @@ | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "albumUsers": { | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/AlbumUserResponseDto" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "assetCount": { | ||||
|             "type": "integer" | ||||
|           }, | ||||
| @ -7349,6 +7414,8 @@ | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "sharedUsers": { | ||||
|             "deprecated": true, | ||||
|             "description": "Deprecated in favor of albumUsers", | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/UserResponseDto" | ||||
|             }, | ||||
| @ -7366,6 +7433,7 @@ | ||||
|         "required": [ | ||||
|           "albumName", | ||||
|           "albumThumbnailAssetId", | ||||
|           "albumUsers", | ||||
|           "assetCount", | ||||
|           "assets", | ||||
|           "createdAt", | ||||
| @ -7381,6 +7449,43 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AlbumUserAddDto": { | ||||
|         "properties": { | ||||
|           "role": { | ||||
|             "$ref": "#/components/schemas/AlbumUserRole" | ||||
|           }, | ||||
|           "userId": { | ||||
|             "format": "uuid", | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "userId" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AlbumUserResponseDto": { | ||||
|         "properties": { | ||||
|           "role": { | ||||
|             "$ref": "#/components/schemas/AlbumUserRole" | ||||
|           }, | ||||
|           "user": { | ||||
|             "$ref": "#/components/schemas/UserResponseDto" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "role", | ||||
|           "user" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AlbumUserRole": { | ||||
|         "enum": [ | ||||
|           "editor", | ||||
|           "viewer" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "AllJobStatusResponseDto": { | ||||
|         "properties": { | ||||
|           "backgroundTask": { | ||||
| @ -11190,6 +11295,17 @@ | ||||
|         }, | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "UpdateAlbumUserDto": { | ||||
|         "properties": { | ||||
|           "role": { | ||||
|             "$ref": "#/components/schemas/AlbumUserRole" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "role" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "UpdateAssetDto": { | ||||
|         "properties": { | ||||
|           "dateTimeOriginal": { | ||||
|  | ||||
| @ -38,6 +38,28 @@ export type ActivityCreateDto = { | ||||
| export type ActivityStatisticsResponseDto = { | ||||
|     comments: number; | ||||
| }; | ||||
| export type UserResponseDto = { | ||||
|     avatarColor: UserAvatarColor; | ||||
|     createdAt: string; | ||||
|     deletedAt: string | null; | ||||
|     email: string; | ||||
|     id: string; | ||||
|     isAdmin: boolean; | ||||
|     memoriesEnabled?: boolean; | ||||
|     name: string; | ||||
|     oauthId: string; | ||||
|     profileImagePath: string; | ||||
|     quotaSizeInBytes: number | null; | ||||
|     quotaUsageInBytes: number | null; | ||||
|     shouldChangePassword: boolean; | ||||
|     status: UserStatus; | ||||
|     storageLabel: string | null; | ||||
|     updatedAt: string; | ||||
| }; | ||||
| export type AlbumUserResponseDto = { | ||||
|     role: AlbumUserRole; | ||||
|     user: UserResponseDto; | ||||
| }; | ||||
| export type ExifResponseDto = { | ||||
|     city?: string | null; | ||||
|     country?: string | null; | ||||
| @ -61,24 +83,6 @@ export type ExifResponseDto = { | ||||
|     state?: string | null; | ||||
|     timeZone?: string | null; | ||||
| }; | ||||
| export type UserResponseDto = { | ||||
|     avatarColor: UserAvatarColor; | ||||
|     createdAt: string; | ||||
|     deletedAt: string | null; | ||||
|     email: string; | ||||
|     id: string; | ||||
|     isAdmin: boolean; | ||||
|     memoriesEnabled?: boolean; | ||||
|     name: string; | ||||
|     oauthId: string; | ||||
|     profileImagePath: string; | ||||
|     quotaSizeInBytes: number | null; | ||||
|     quotaUsageInBytes: number | null; | ||||
|     shouldChangePassword: boolean; | ||||
|     status: UserStatus; | ||||
|     storageLabel: string | null; | ||||
|     updatedAt: string; | ||||
| }; | ||||
| export type AssetFaceWithoutPersonResponseDto = { | ||||
|     boundingBoxX1: number; | ||||
|     boundingBoxX2: number; | ||||
| @ -144,6 +148,7 @@ export type AssetResponseDto = { | ||||
| export type AlbumResponseDto = { | ||||
|     albumName: string; | ||||
|     albumThumbnailAssetId: string | null; | ||||
|     albumUsers: AlbumUserResponseDto[]; | ||||
|     assetCount: number; | ||||
|     assets: AssetResponseDto[]; | ||||
|     createdAt: string; | ||||
| @ -157,6 +162,7 @@ export type AlbumResponseDto = { | ||||
|     owner: UserResponseDto; | ||||
|     ownerId: string; | ||||
|     shared: boolean; | ||||
|     /** Deprecated in favor of albumUsers */ | ||||
|     sharedUsers: UserResponseDto[]; | ||||
|     startDate?: string; | ||||
|     updatedAt: string; | ||||
| @ -187,8 +193,17 @@ export type BulkIdResponseDto = { | ||||
|     id: string; | ||||
|     success: boolean; | ||||
| }; | ||||
| export type UpdateAlbumUserDto = { | ||||
|     role: AlbumUserRole; | ||||
| }; | ||||
| export type AlbumUserAddDto = { | ||||
|     role?: AlbumUserRole; | ||||
|     userId: string; | ||||
| }; | ||||
| export type AddUsersDto = { | ||||
|     sharedUserIds: string[]; | ||||
|     albumUsers: AlbumUserAddDto[]; | ||||
|     /** Deprecated in favor of albumUsers */ | ||||
|     sharedUserIds?: string[]; | ||||
| }; | ||||
| export type ApiKeyResponseDto = { | ||||
|     createdAt: string; | ||||
| @ -1209,6 +1224,17 @@ export function removeUserFromAlbum({ id, userId }: { | ||||
|         method: "DELETE" | ||||
|     })); | ||||
| } | ||||
| export function updateAlbumUser({ id, userId, updateAlbumUserDto }: { | ||||
|     id: string; | ||||
|     userId: string; | ||||
|     updateAlbumUserDto: UpdateAlbumUserDto; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchText(`/album/${encodeURIComponent(id)}/user/${encodeURIComponent(userId)}`, oazapfts.json({ | ||||
|         ...opts, | ||||
|         method: "PUT", | ||||
|         body: updateAlbumUserDto | ||||
|     }))); | ||||
| } | ||||
| export function addUsersToAlbum({ id, addUsersDto }: { | ||||
|     id: string; | ||||
|     addUsersDto: AddUsersDto; | ||||
| @ -2927,6 +2953,10 @@ export enum UserAvatarColor { | ||||
|     Gray = "gray", | ||||
|     Amber = "amber" | ||||
| } | ||||
| export enum AlbumUserRole { | ||||
|     Editor = "editor", | ||||
|     Viewer = "viewer" | ||||
| } | ||||
| export enum UserStatus { | ||||
|     Active = "active", | ||||
|     Removing = "removing", | ||||
|  | ||||
| @ -8,6 +8,7 @@ import { | ||||
|   CreateAlbumDto, | ||||
|   GetAlbumsDto, | ||||
|   UpdateAlbumDto, | ||||
|   UpdateAlbumUserDto, | ||||
| } from 'src/dtos/album.dto'; | ||||
| import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| @ -88,6 +89,16 @@ export class AlbumController { | ||||
|     return this.service.addUsers(auth, id, dto); | ||||
|   } | ||||
| 
 | ||||
|   @Put(':id/user/:userId') | ||||
|   updateAlbumUser( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|     @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, | ||||
|     @Body() dto: UpdateAlbumUserDto, | ||||
|   ): Promise<void> { | ||||
|     return this.service.updateUser(auth, id, userId, dto); | ||||
|   } | ||||
| 
 | ||||
|   @Delete(':id/user/:userId') | ||||
|   removeUserFromAlbum( | ||||
|     @Auth() auth: AuthDto, | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { BadRequestException, UnauthorizedException } from '@nestjs/common'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { SharedLinkEntity } from 'src/entities/shared-link.entity'; | ||||
| import { IAccessRepository } from 'src/interfaces/access.interface'; | ||||
| import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; | ||||
| @ -22,6 +23,7 @@ export enum Permission { | ||||
|   ALBUM_READ = 'album.read', | ||||
|   ALBUM_UPDATE = 'album.update', | ||||
|   ALBUM_DELETE = 'album.delete', | ||||
|   ALBUM_ADD_ASSET = 'album.addAsset', | ||||
|   ALBUM_REMOVE_ASSET = 'album.removeAsset', | ||||
|   ALBUM_SHARE = 'album.share', | ||||
|   ALBUM_DOWNLOAD = 'album.download', | ||||
| @ -142,6 +144,12 @@ export class AccessCore { | ||||
|           : new Set(); | ||||
|       } | ||||
| 
 | ||||
|       case Permission.ALBUM_ADD_ASSET: { | ||||
|         return sharedLink.allowUpload | ||||
|           ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) | ||||
|           : new Set(); | ||||
|       } | ||||
| 
 | ||||
|       default: { | ||||
|         return new Set(); | ||||
|       } | ||||
| @ -215,7 +223,21 @@ export class AccessCore { | ||||
| 
 | ||||
|       case Permission.ALBUM_READ: { | ||||
|         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess( | ||||
|           auth.user.id, | ||||
|           setDifference(ids, isOwner), | ||||
|           AlbumUserRole.VIEWER, | ||||
|         ); | ||||
|         return setUnion(isOwner, isShared); | ||||
|       } | ||||
| 
 | ||||
|       case Permission.ALBUM_ADD_ASSET: { | ||||
|         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess( | ||||
|           auth.user.id, | ||||
|           setDifference(ids, isOwner), | ||||
|           AlbumUserRole.EDITOR, | ||||
|         ); | ||||
|         return setUnion(isOwner, isShared); | ||||
|       } | ||||
| 
 | ||||
| @ -233,12 +255,22 @@ export class AccessCore { | ||||
| 
 | ||||
|       case Permission.ALBUM_DOWNLOAD: { | ||||
|         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess( | ||||
|           auth.user.id, | ||||
|           setDifference(ids, isOwner), | ||||
|           AlbumUserRole.VIEWER, | ||||
|         ); | ||||
|         return setUnion(isOwner, isShared); | ||||
|       } | ||||
| 
 | ||||
|       case Permission.ALBUM_REMOVE_ASSET: { | ||||
|         return await this.repository.album.checkOwnerAccess(auth.user.id, ids); | ||||
|         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); | ||||
|         const isShared = await this.repository.album.checkSharedAlbumAccess( | ||||
|           auth.user.id, | ||||
|           setDifference(ids, isOwner), | ||||
|           AlbumUserRole.EDITOR, | ||||
|         ); | ||||
|         return setUnion(isOwner, isShared); | ||||
|       } | ||||
| 
 | ||||
|       case Permission.ASSET_UPLOAD: { | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; | ||||
| import _ from 'lodash'; | ||||
| import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; | ||||
| import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; | ||||
| 
 | ||||
| @ -11,10 +13,23 @@ export class AlbumInfoDto { | ||||
|   withoutAssets?: boolean; | ||||
| } | ||||
| 
 | ||||
| export class AlbumUserAddDto { | ||||
|   @ValidateUUID() | ||||
|   userId!: string; | ||||
| 
 | ||||
|   @IsEnum(AlbumUserRole) | ||||
|   @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) | ||||
|   role?: AlbumUserRole; | ||||
| } | ||||
| 
 | ||||
| export class AddUsersDto { | ||||
|   @ValidateUUID({ each: true }) | ||||
|   @ValidateUUID({ each: true, optional: true }) | ||||
|   @ArrayNotEmpty() | ||||
|   sharedUserIds!: string[]; | ||||
|   @ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' }) | ||||
|   sharedUserIds?: string[]; | ||||
| 
 | ||||
|   @ArrayNotEmpty() | ||||
|   albumUsers!: AlbumUserAddDto[]; | ||||
| } | ||||
| 
 | ||||
| export class CreateAlbumDto { | ||||
| @ -83,6 +98,18 @@ export class AlbumCountResponseDto { | ||||
|   notShared!: number; | ||||
| } | ||||
| 
 | ||||
| export class UpdateAlbumUserDto { | ||||
|   @IsEnum(AlbumUserRole) | ||||
|   @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) | ||||
|   role!: AlbumUserRole; | ||||
| } | ||||
| 
 | ||||
| export class AlbumUserResponseDto { | ||||
|   user!: UserResponseDto; | ||||
|   @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) | ||||
|   role!: AlbumUserRole; | ||||
| } | ||||
| 
 | ||||
| export class AlbumResponseDto { | ||||
|   id!: string; | ||||
|   ownerId!: string; | ||||
| @ -92,7 +119,9 @@ export class AlbumResponseDto { | ||||
|   updatedAt!: Date; | ||||
|   albumThumbnailAssetId!: string | null; | ||||
|   shared!: boolean; | ||||
|   @ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' }) | ||||
|   sharedUsers!: UserResponseDto[]; | ||||
|   albumUsers!: AlbumUserResponseDto[]; | ||||
|   hasSharedLink!: boolean; | ||||
|   assets!: AssetResponseDto[]; | ||||
|   owner!: UserResponseDto; | ||||
| @ -109,13 +138,21 @@ export class AlbumResponseDto { | ||||
| 
 | ||||
| export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { | ||||
|   const sharedUsers: UserResponseDto[] = []; | ||||
|   const albumUsers: AlbumUserResponseDto[] = []; | ||||
| 
 | ||||
|   if (entity.sharedUsers) { | ||||
|     for (const user of entity.sharedUsers) { | ||||
|       sharedUsers.push(mapUser(user)); | ||||
|   if (entity.albumUsers) { | ||||
|     for (const albumUser of entity.albumUsers) { | ||||
|       const user = mapUser(albumUser.user); | ||||
|       sharedUsers.push(user); | ||||
|       albumUsers.push({ | ||||
|         user, | ||||
|         role: albumUser.role, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const albumUsersSorted = _.orderBy(albumUsers, ['role', 'user.name']); | ||||
| 
 | ||||
|   const assets = entity.assets || []; | ||||
| 
 | ||||
|   const hasSharedLink = entity.sharedLinks?.length > 0; | ||||
| @ -138,6 +175,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt | ||||
|     ownerId: entity.ownerId, | ||||
|     owner: mapUser(entity.owner), | ||||
|     sharedUsers, | ||||
|     albumUsers: albumUsersSorted, | ||||
|     shared: hasSharedUser || hasSharedLink, | ||||
|     hasSharedLink, | ||||
|     startDate, | ||||
|  | ||||
							
								
								
									
										31
									
								
								server/src/entities/album-user.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								server/src/entities/album-user.entity.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| import { AlbumEntity } from 'src/entities/album.entity'; | ||||
| import { UserEntity } from 'src/entities/user.entity'; | ||||
| import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; | ||||
| 
 | ||||
| export enum AlbumUserRole { | ||||
|   EDITOR = 'editor', | ||||
|   VIEWER = 'viewer', | ||||
| } | ||||
| 
 | ||||
| @Entity('albums_shared_users_users') | ||||
| // Pre-existing indices from original album <--> user ManyToMany mapping
 | ||||
| @Index('IDX_427c350ad49bd3935a50baab73', ['album']) | ||||
| @Index('IDX_f48513bf9bccefd6ff3ad30bd0', ['user']) | ||||
| export class AlbumUserEntity { | ||||
|   @PrimaryColumn({ type: 'uuid', name: 'albumsId' }) | ||||
|   albumId!: string; | ||||
| 
 | ||||
|   @PrimaryColumn({ type: 'uuid', name: 'usersId' }) | ||||
|   userId!: string; | ||||
| 
 | ||||
|   @JoinColumn({ name: 'albumsId' }) | ||||
|   @ManyToOne(() => AlbumEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) | ||||
|   album!: AlbumEntity; | ||||
| 
 | ||||
|   @JoinColumn({ name: 'usersId' }) | ||||
|   @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) | ||||
|   user!: UserEntity; | ||||
| 
 | ||||
|   @Column({ type: 'varchar', default: AlbumUserRole.EDITOR }) | ||||
|   role!: AlbumUserRole; | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| import { AlbumUserEntity } from 'src/entities/album-user.entity'; | ||||
| import { AssetEntity } from 'src/entities/asset.entity'; | ||||
| import { SharedLinkEntity } from 'src/entities/shared-link.entity'; | ||||
| import { UserEntity } from 'src/entities/user.entity'; | ||||
| @ -52,9 +53,8 @@ export class AlbumEntity { | ||||
|   @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true }) | ||||
|   albumThumbnailAssetId!: string | null; | ||||
| 
 | ||||
|   @ManyToMany(() => UserEntity) | ||||
|   @JoinTable() | ||||
|   sharedUsers!: UserEntity[]; | ||||
|   @OneToMany(() => AlbumUserEntity, ({ album }) => album, { cascade: true, onDelete: 'CASCADE' }) | ||||
|   albumUsers!: AlbumUserEntity[]; | ||||
| 
 | ||||
|   @ManyToMany(() => AssetEntity, (asset) => asset.albums) | ||||
|   @JoinTable() | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { ActivityEntity } from 'src/entities/activity.entity'; | ||||
| import { AlbumUserEntity } from 'src/entities/album-user.entity'; | ||||
| import { AlbumEntity } from 'src/entities/album.entity'; | ||||
| import { APIKeyEntity } from 'src/entities/api-key.entity'; | ||||
| import { AssetFaceEntity } from 'src/entities/asset-face.entity'; | ||||
| @ -25,6 +26,7 @@ import { UserEntity } from 'src/entities/user.entity'; | ||||
| export const entities = [ | ||||
|   ActivityEntity, | ||||
|   AlbumEntity, | ||||
|   AlbumUserEntity, | ||||
|   APIKeyEntity, | ||||
|   AssetEntity, | ||||
|   AssetStackEntity, | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| 
 | ||||
| export const IAccessRepository = 'IAccessRepository'; | ||||
| 
 | ||||
| export interface IAccessRepository { | ||||
| @ -20,7 +22,7 @@ export interface IAccessRepository { | ||||
| 
 | ||||
|   album: { | ||||
|     checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>; | ||||
|     checkSharedAlbumAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>; | ||||
|     checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>; | ||||
|     checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>; | ||||
|   }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										14
									
								
								server/src/interfaces/album-user.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/src/interfaces/album-user.interface.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| import { AlbumUserEntity } from 'src/entities/album-user.entity'; | ||||
| 
 | ||||
| export const IAlbumUserRepository = 'IAlbumUserRepository'; | ||||
| 
 | ||||
| export type AlbumPermissionId = { | ||||
|   albumId: string; | ||||
|   userId: string; | ||||
| }; | ||||
| 
 | ||||
| export interface IAlbumUserRepository { | ||||
|   create(albumUser: Partial<AlbumUserEntity>): Promise<AlbumUserEntity>; | ||||
|   update({ userId, albumId }: AlbumPermissionId, albumPermission: Partial<AlbumUserEntity>): Promise<AlbumUserEntity>; | ||||
|   delete({ userId, albumId }: AlbumPermissionId): Promise<void>; | ||||
| } | ||||
							
								
								
									
										14
									
								
								server/src/migrations/1713337511945-AddAlbumUserRole.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/src/migrations/1713337511945-AddAlbumUserRole.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
| 
 | ||||
| export class AddAlbumUserRole1713337511945 implements MigrationInterface { | ||||
|     name = 'AddAlbumUserRole1713337511945' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "role" character varying NOT NULL DEFAULT 'editor'`); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "role"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -37,16 +37,16 @@ SELECT | ||||
|   "album"."id" AS "album_id" | ||||
| FROM | ||||
|   "albums" "album" | ||||
|   LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id" | ||||
|   LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId" | ||||
|   AND ("sharedUsers"."deletedAt" IS NULL) | ||||
|   LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id" | ||||
|   LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId" | ||||
|   AND ("albumUsers"."deletedAt" IS NULL) | ||||
| WHERE | ||||
|   ( | ||||
|     "album"."id" IN ($1) | ||||
|     AND "album"."isActivityEnabled" = true | ||||
|     AND ( | ||||
|       "album"."ownerId" = $2 | ||||
|       OR "sharedUsers"."id" = $2 | ||||
|       OR "albumUsers"."id" = $2 | ||||
|     ) | ||||
|   ) | ||||
|   AND ("album"."deletedAt" IS NULL) | ||||
| @ -70,10 +70,10 @@ SELECT | ||||
|   "AlbumEntity"."id" AS "AlbumEntity_id" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|     "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|   ) | ||||
| WHERE | ||||
|   ( | ||||
| @ -81,7 +81,16 @@ WHERE | ||||
|       ("AlbumEntity"."id" IN ($1)) | ||||
|       AND ( | ||||
|         ( | ||||
|           ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $2) | ||||
|           ( | ||||
|             ( | ||||
|               ( | ||||
|                 "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = $2 | ||||
|               ) | ||||
|             ) | ||||
|           ) | ||||
|           AND ( | ||||
|             "AlbumEntity__AlbumEntity_albumUsers"."role" IN ($3, $4) | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|     ) | ||||
| @ -109,15 +118,15 @@ FROM | ||||
|   INNER JOIN "albums_assets_assets" "album_asset" ON "album_asset"."albumsId" = "album"."id" | ||||
|   INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId" | ||||
|   AND ("asset"."deletedAt" IS NULL) | ||||
|   LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id" | ||||
|   LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId" | ||||
|   AND ("sharedUsers"."deletedAt" IS NULL) | ||||
|   LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id" | ||||
|   LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId" | ||||
|   AND ("albumUsers"."deletedAt" IS NULL) | ||||
| WHERE | ||||
|   ( | ||||
|     array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid [] | ||||
|     AND ( | ||||
|       "album"."ownerId" = $2 | ||||
|       OR "sharedUsers"."id" = $2 | ||||
|       OR "albumUsers"."id" = $2 | ||||
|     ) | ||||
|   ) | ||||
|   AND ("album"."deletedAt" IS NULL) | ||||
|  | ||||
| @ -32,22 +32,25 @@ FROM | ||||
|       "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"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|       "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|       "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|       "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|       "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", | ||||
|       "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", | ||||
|       "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", | ||||
|       "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", | ||||
|       "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", | ||||
| @ -66,10 +69,10 @@ FROM | ||||
|       AND ( | ||||
|         "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL | ||||
|       ) | ||||
|       LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|       LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|       LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|       LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|       AND ( | ||||
|         "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|         "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|       ) | ||||
|       LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" | ||||
|     WHERE | ||||
| @ -109,32 +112,35 @@ SELECT | ||||
|   "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"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|   "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|     "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|   ) | ||||
| WHERE | ||||
|   ((("AlbumEntity"."id" IN ($1)))) | ||||
| @ -168,32 +174,35 @@ SELECT | ||||
|   "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"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|   "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|     "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId" | ||||
| @ -213,7 +222,9 @@ WHERE | ||||
|         ( | ||||
|           ( | ||||
|             ( | ||||
|               ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $3) | ||||
|               ( | ||||
|                 "AlbumEntity__AlbumEntity_albumUsers"."usersId" = $3 | ||||
|               ) | ||||
|             ) | ||||
|           ) | ||||
|           AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4))) | ||||
| @ -283,22 +294,25 @@ SELECT | ||||
|   "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", | ||||
|   "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", | ||||
|   "AlbumEntity"."order" AS "AlbumEntity_order", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|   "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", | ||||
| @ -329,10 +343,10 @@ SELECT | ||||
|   "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|     "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" | ||||
| @ -357,22 +371,25 @@ SELECT | ||||
|   "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", | ||||
|   "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", | ||||
|   "AlbumEntity"."order" AS "AlbumEntity_order", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|   "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", | ||||
|   "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", | ||||
| @ -403,10 +420,10 @@ SELECT | ||||
|   "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|     "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" | ||||
| @ -420,7 +437,9 @@ WHERE | ||||
|         ( | ||||
|           ( | ||||
|             ( | ||||
|               ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $1) | ||||
|               ( | ||||
|                 "AlbumEntity__AlbumEntity_albumUsers"."usersId" = $1 | ||||
|               ) | ||||
|             ) | ||||
|           ) | ||||
|         ) | ||||
| @ -443,7 +462,7 @@ WHERE | ||||
|             ( | ||||
|               ( | ||||
|                 NOT ( | ||||
|                   "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL | ||||
|                   "AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL | ||||
|                 ) | ||||
|               ) | ||||
|             ) | ||||
| @ -468,22 +487,9 @@ SELECT | ||||
|   "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", | ||||
|   "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", | ||||
|   "AlbumEntity"."order" AS "AlbumEntity_order", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status", | ||||
|   "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", | ||||
|   "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_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", | ||||
|   "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", | ||||
|   "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", | ||||
| @ -514,11 +520,7 @@ SELECT | ||||
|   "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" | ||||
| FROM | ||||
|   "albums" "AlbumEntity" | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" | ||||
|   AND ( | ||||
|     "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL | ||||
|   ) | ||||
|   LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" | ||||
|   LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" | ||||
|   AND ( | ||||
| @ -531,7 +533,7 @@ WHERE | ||||
|       AND ( | ||||
|         ( | ||||
|           ( | ||||
|             "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL | ||||
|             "AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { ActivityEntity } from 'src/entities/activity.entity'; | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { AlbumEntity } from 'src/entities/album.entity'; | ||||
| import { AssetFaceEntity } from 'src/entities/asset-face.entity'; | ||||
| import { AssetEntity } from 'src/entities/asset.entity'; | ||||
| @ -81,12 +82,13 @@ class ActivityAccess implements IActivityAccess { | ||||
|     return this.albumRepository | ||||
|       .createQueryBuilder('album') | ||||
|       .select('album.id') | ||||
|       .leftJoin('album.sharedUsers', 'sharedUsers') | ||||
|       .leftJoin('album.albumUsers', 'album_albumUsers_users') | ||||
|       .leftJoin('album_albumUsers_users.user', 'albumUsers') | ||||
|       .where('album.id IN (:...albumIds)', { albumIds: [...albumIds] }) | ||||
|       .andWhere('album.isActivityEnabled = true') | ||||
|       .andWhere( | ||||
|         new Brackets((qb) => { | ||||
|           qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); | ||||
|           qb.where('album.ownerId = :userId', { userId }).orWhere('albumUsers.id = :userId', { userId }); | ||||
|         }), | ||||
|       ) | ||||
|       .getMany() | ||||
| @ -120,7 +122,7 @@ class AlbumAccess implements IAlbumAccess { | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) | ||||
|   @ChunkedSet({ paramIndex: 1 }) | ||||
|   async checkSharedAlbumAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> { | ||||
|   async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> { | ||||
|     if (albumIds.size === 0) { | ||||
|       return new Set(); | ||||
|     } | ||||
| @ -130,8 +132,11 @@ class AlbumAccess implements IAlbumAccess { | ||||
|         select: { id: true }, | ||||
|         where: { | ||||
|           id: In([...albumIds]), | ||||
|           sharedUsers: { | ||||
|             id: userId, | ||||
|           albumUsers: { | ||||
|             user: { id: userId }, | ||||
|             // If editor access is needed we check for it, otherwise both are accepted
 | ||||
|             role: | ||||
|               access === AlbumUserRole.EDITOR ? AlbumUserRole.EDITOR : In([AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]), | ||||
|           }, | ||||
|         }, | ||||
|       }) | ||||
| @ -177,7 +182,8 @@ class AssetAccess implements IAssetAccess { | ||||
|     return this.albumRepository | ||||
|       .createQueryBuilder('album') | ||||
|       .innerJoin('album.assets', 'asset') | ||||
|       .leftJoin('album.sharedUsers', 'sharedUsers') | ||||
|       .leftJoin('album.albumUsers', 'album_albumUsers_users') | ||||
|       .leftJoin('album_albumUsers_users.user', 'albumUsers') | ||||
|       .select('asset.id', 'assetId') | ||||
|       .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') | ||||
|       .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { | ||||
| @ -185,7 +191,7 @@ class AssetAccess implements IAssetAccess { | ||||
|       }) | ||||
|       .andWhere( | ||||
|         new Brackets((qb) => { | ||||
|           qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); | ||||
|           qb.where('album.ownerId = :userId', { userId }).orWhere('albumUsers.id = :userId', { userId }); | ||||
|         }), | ||||
|       ) | ||||
|       .getRawMany() | ||||
|  | ||||
							
								
								
									
										28
									
								
								server/src/repositories/album-user.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/src/repositories/album-user.repository.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { AlbumUserEntity } from 'src/entities/album-user.entity'; | ||||
| import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface'; | ||||
| import { Instrumentation } from 'src/utils/instrumentation'; | ||||
| import { Repository } from 'typeorm'; | ||||
| 
 | ||||
| @Instrumentation() | ||||
| @Injectable() | ||||
| export class AlbumUserRepository implements IAlbumUserRepository { | ||||
|   constructor(@InjectRepository(AlbumUserEntity) private repository: Repository<AlbumUserEntity>) {} | ||||
| 
 | ||||
|   async create(albumUser: Partial<AlbumUserEntity>): Promise<AlbumUserEntity> { | ||||
|     const { userId, albumId } = await this.repository.save(albumUser); | ||||
|     return this.repository.findOneOrFail({ where: { userId, albumId } }); | ||||
|   } | ||||
| 
 | ||||
|   async update({ userId, albumId }: AlbumPermissionId, dto: Partial<AlbumUserEntity>): Promise<AlbumUserEntity> { | ||||
|     await this.repository.update({ userId, albumId }, dto); | ||||
|     return this.repository.findOneOrFail({ | ||||
|       where: { userId, albumId }, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async delete({ userId, albumId }: AlbumPermissionId): Promise<void> { | ||||
|     await this.repository.delete({ userId, albumId }); | ||||
|   } | ||||
| } | ||||
| @ -10,6 +10,13 @@ import { Instrumentation } from 'src/utils/instrumentation'; | ||||
| import { setUnion } from 'src/utils/set'; | ||||
| import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; | ||||
| 
 | ||||
| const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => { | ||||
|   if (album) { | ||||
|     album.albumUsers = album.albumUsers.filter((albumUser) => albumUser.user && !albumUser.user.deletedAt); | ||||
|   } | ||||
|   return album; | ||||
| }; | ||||
| 
 | ||||
| @Instrumentation() | ||||
| @Injectable() | ||||
| export class AlbumRepository implements IAlbumRepository { | ||||
| @ -20,10 +27,10 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|   ) {} | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID, {}] }) | ||||
|   getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> { | ||||
|   async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> { | ||||
|     const relations: FindOptionsRelations<AlbumEntity> = { | ||||
|       owner: true, | ||||
|       sharedUsers: true, | ||||
|       albumUsers: { user: true }, | ||||
|       assets: false, | ||||
|       sharedLinks: true, | ||||
|     }; | ||||
| @ -40,33 +47,38 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     return this.repository.findOne({ where: { id }, relations, order }); | ||||
|     const album = await this.repository.findOne({ where: { id }, relations, order }); | ||||
|     return withoutDeletedUsers(album); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [[DummyValue.UUID]] }) | ||||
|   @ChunkedArray() | ||||
|   getByIds(ids: string[]): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
|   async getByIds(ids: string[]): Promise<AlbumEntity[]> { | ||||
|     const albums = await this.repository.find({ | ||||
|       where: { | ||||
|         id: In(ids), | ||||
|       }, | ||||
|       relations: { | ||||
|         owner: true, | ||||
|         sharedUsers: true, | ||||
|         albumUsers: { user: true }, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     return albums.map((album) => withoutDeletedUsers(album)); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) | ||||
|   getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
|   async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> { | ||||
|     const albums = await this.repository.find({ | ||||
|       where: [ | ||||
|         { ownerId, assets: { id: assetId } }, | ||||
|         { sharedUsers: { id: ownerId }, assets: { id: assetId } }, | ||||
|         { albumUsers: { userId: ownerId }, assets: { id: assetId } }, | ||||
|       ], | ||||
|       relations: { owner: true, sharedUsers: true }, | ||||
|       relations: { owner: true, albumUsers: { user: true } }, | ||||
|       order: { createdAt: 'DESC' }, | ||||
|     }); | ||||
| 
 | ||||
|     return albums.map((album) => withoutDeletedUsers(album)); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [[DummyValue.UUID]] }) | ||||
| @ -127,40 +139,46 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getOwned(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       relations: { sharedUsers: true, sharedLinks: true, owner: true }, | ||||
|   async getOwned(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     const albums = await this.repository.find({ | ||||
|       relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, | ||||
|       where: { ownerId }, | ||||
|       order: { createdAt: 'DESC' }, | ||||
|     }); | ||||
| 
 | ||||
|     return albums.map((album) => withoutDeletedUsers(album)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get albums shared with and shared by owner. | ||||
|    */ | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getShared(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       relations: { sharedUsers: true, sharedLinks: true, owner: true }, | ||||
|   async getShared(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     const albums = await this.repository.find({ | ||||
|       relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, | ||||
|       where: [ | ||||
|         { sharedUsers: { id: ownerId } }, | ||||
|         { albumUsers: { userId: ownerId } }, | ||||
|         { sharedLinks: { userId: ownerId } }, | ||||
|         { ownerId, sharedUsers: { id: Not(IsNull()) } }, | ||||
|         { ownerId, albumUsers: { user: Not(IsNull()) } }, | ||||
|       ], | ||||
|       order: { createdAt: 'DESC' }, | ||||
|     }); | ||||
| 
 | ||||
|     return albums.map((album) => withoutDeletedUsers(album)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get albums of owner that are _not_ shared | ||||
|    */ | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getNotShared(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       relations: { sharedUsers: true, sharedLinks: true, owner: true }, | ||||
|       where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } }, | ||||
|   async getNotShared(ownerId: string): Promise<AlbumEntity[]> { | ||||
|     const albums = await this.repository.find({ | ||||
|       relations: { albumUsers: true, sharedLinks: true, owner: true }, | ||||
|       where: { ownerId, albumUsers: { user: IsNull() }, sharedLinks: { id: IsNull() } }, | ||||
|       order: { createdAt: 'DESC' }, | ||||
|     }); | ||||
| 
 | ||||
|     return albums.map((album) => withoutDeletedUsers(album)); | ||||
|   } | ||||
| 
 | ||||
|   async restoreAll(userId: string): Promise<void> { | ||||
| @ -282,7 +300,7 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|       where: { id }, | ||||
|       relations: { | ||||
|         owner: true, | ||||
|         sharedUsers: true, | ||||
|         albumUsers: { user: true }, | ||||
|         sharedLinks: true, | ||||
|         assets: true, | ||||
|       }, | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { IAccessRepository } from 'src/interfaces/access.interface'; | ||||
| import { IActivityRepository } from 'src/interfaces/activity.interface'; | ||||
| import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; | ||||
| import { IAlbumRepository } from 'src/interfaces/album.interface'; | ||||
| import { IKeyRepository } from 'src/interfaces/api-key.interface'; | ||||
| import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; | ||||
| @ -31,6 +32,7 @@ import { ITagRepository } from 'src/interfaces/tag.interface'; | ||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | ||||
| import { AccessRepository } from 'src/repositories/access.repository'; | ||||
| import { ActivityRepository } from 'src/repositories/activity.repository'; | ||||
| import { AlbumUserRepository } from 'src/repositories/album-user.repository'; | ||||
| import { AlbumRepository } from 'src/repositories/album.repository'; | ||||
| import { ApiKeyRepository } from 'src/repositories/api-key.repository'; | ||||
| import { AssetStackRepository } from 'src/repositories/asset-stack.repository'; | ||||
| @ -65,6 +67,7 @@ export const repositories = [ | ||||
|   { provide: IActivityRepository, useClass: ActivityRepository }, | ||||
|   { provide: IAccessRepository, useClass: AccessRepository }, | ||||
|   { provide: IAlbumRepository, useClass: AlbumRepository }, | ||||
|   { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, | ||||
|   { provide: IAssetRepository, useClass: AssetRepository }, | ||||
|   { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 }, | ||||
|   { provide: IAssetStackRepository, useClass: AssetStackRepository }, | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import _ from 'lodash'; | ||||
| import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; | ||||
| import { IAlbumRepository } from 'src/interfaces/album.interface'; | ||||
| import { IAssetRepository } from 'src/interfaces/asset.interface'; | ||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | ||||
| @ -9,6 +11,7 @@ import { albumStub } from 'test/fixtures/album.stub'; | ||||
| import { authStub } from 'test/fixtures/auth.stub'; | ||||
| import { userStub } from 'test/fixtures/user.stub'; | ||||
| import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; | ||||
| import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; | ||||
| import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; | ||||
| import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; | ||||
| import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; | ||||
| @ -20,14 +23,16 @@ describe(AlbumService.name, () => { | ||||
|   let albumMock: Mocked<IAlbumRepository>; | ||||
|   let assetMock: Mocked<IAssetRepository>; | ||||
|   let userMock: Mocked<IUserRepository>; | ||||
|   let albumUserMock: Mocked<IAlbumUserRepository>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     accessMock = newAccessRepositoryMock(); | ||||
|     albumMock = newAlbumRepositoryMock(); | ||||
|     assetMock = newAssetRepositoryMock(); | ||||
|     userMock = newUserRepositoryMock(); | ||||
|     albumUserMock = newAlbumUserRepositoryMock(); | ||||
| 
 | ||||
|     sut = new AlbumService(accessMock, albumMock, assetMock, userMock); | ||||
|     sut = new AlbumService(accessMock, albumMock, assetMock, userMock, albumUserMock); | ||||
|   }); | ||||
| 
 | ||||
|   it('should work', () => { | ||||
| @ -189,7 +194,7 @@ describe(AlbumService.name, () => { | ||||
|         ownerId: authStub.admin.user.id, | ||||
|         albumName: albumStub.empty.albumName, | ||||
|         description: albumStub.empty.description, | ||||
|         sharedUsers: [{ id: 'user-id' }], | ||||
|         albumUsers: [{ user: { id: 'user-id' } }], | ||||
|         assets: [{ id: '123' }], | ||||
|         albumThumbnailAssetId: '123', | ||||
|       }); | ||||
| @ -225,7 +230,7 @@ describe(AlbumService.name, () => { | ||||
|         ownerId: authStub.admin.user.id, | ||||
|         albumName: 'Test album', | ||||
|         description: '', | ||||
|         sharedUsers: [], | ||||
|         albumUsers: [], | ||||
|         assets: [{ id: 'asset-1' }], | ||||
|         albumThumbnailAssetId: 'asset-1', | ||||
|       }); | ||||
| @ -327,7 +332,7 @@ describe(AlbumService.name, () => { | ||||
|   describe('addUsers', () => { | ||||
|     it('should throw an error if the auth user is not the owner', async () => { | ||||
|       await expect( | ||||
|         sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-1'] }), | ||||
|         sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-1' }] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| @ -336,7 +341,9 @@ describe(AlbumService.name, () => { | ||||
|       accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       await expect( | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.user.id] }), | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { | ||||
|           albumUsers: [{ userId: authStub.admin.user.id }], | ||||
|         }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| @ -346,7 +353,7 @@ describe(AlbumService.name, () => { | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       userMock.get.mockResolvedValue(null); | ||||
|       await expect( | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-3'] }), | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-3' }] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| @ -356,11 +363,19 @@ describe(AlbumService.name, () => { | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); | ||||
|       albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       userMock.get.mockResolvedValue(userStub.user2); | ||||
|       await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.user.id] }); | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: albumStub.sharedWithAdmin.id, | ||||
|         updatedAt: expect.any(Date), | ||||
|         sharedUsers: [userStub.admin, { id: authStub.user2.user.id }], | ||||
|       albumUserMock.create.mockResolvedValue({ | ||||
|         userId: userStub.user2.id, | ||||
|         user: userStub.user2, | ||||
|         albumId: albumStub.sharedWithAdmin.id, | ||||
|         album: albumStub.sharedWithAdmin, | ||||
|         role: AlbumUserRole.EDITOR, | ||||
|       }); | ||||
|       await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { | ||||
|         albumUsers: [{ userId: authStub.user2.user.id }], | ||||
|       }); | ||||
|       expect(albumUserMock.create).toHaveBeenCalledWith({ | ||||
|         userId: authStub.user2.user.id, | ||||
|         albumId: albumStub.sharedWithAdmin.id, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @ -381,11 +396,10 @@ describe(AlbumService.name, () => { | ||||
|         sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id), | ||||
|       ).resolves.toBeUndefined(); | ||||
| 
 | ||||
|       expect(albumMock.update).toHaveBeenCalledTimes(1); | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: albumStub.sharedWithUser.id, | ||||
|         updatedAt: expect.any(Date), | ||||
|         sharedUsers: [], | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledTimes(1); | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledWith({ | ||||
|         albumId: albumStub.sharedWithUser.id, | ||||
|         userId: userStub.user1.id, | ||||
|       }); | ||||
|       expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false }); | ||||
|     }); | ||||
| @ -397,7 +411,7 @@ describe(AlbumService.name, () => { | ||||
|         sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
| 
 | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|       expect(albumUserMock.delete).not.toHaveBeenCalled(); | ||||
|       expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( | ||||
|         authStub.user1.user.id, | ||||
|         new Set([albumStub.sharedWithMultiple.id]), | ||||
| @ -409,11 +423,10 @@ describe(AlbumService.name, () => { | ||||
| 
 | ||||
|       await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id); | ||||
| 
 | ||||
|       expect(albumMock.update).toHaveBeenCalledTimes(1); | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: albumStub.sharedWithUser.id, | ||||
|         updatedAt: expect.any(Date), | ||||
|         sharedUsers: [], | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledTimes(1); | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledWith({ | ||||
|         albumId: albumStub.sharedWithUser.id, | ||||
|         userId: authStub.user1.user.id, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @ -422,11 +435,10 @@ describe(AlbumService.name, () => { | ||||
| 
 | ||||
|       await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me'); | ||||
| 
 | ||||
|       expect(albumMock.update).toHaveBeenCalledTimes(1); | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: albumStub.sharedWithUser.id, | ||||
|         updatedAt: expect.any(Date), | ||||
|         sharedUsers: [], | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledTimes(1); | ||||
|       expect(albumUserMock.delete).toHaveBeenCalledWith({ | ||||
|         albumId: albumStub.sharedWithUser.id, | ||||
|         userId: authStub.user1.user.id, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @ -512,6 +524,7 @@ describe(AlbumService.name, () => { | ||||
|       expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith( | ||||
|         authStub.user1.user.id, | ||||
|         new Set(['album-123']), | ||||
|         AlbumUserRole.VIEWER, | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
| @ -522,6 +535,7 @@ describe(AlbumService.name, () => { | ||||
|       expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith( | ||||
|         authStub.admin.user.id, | ||||
|         new Set(['album-123']), | ||||
|         AlbumUserRole.VIEWER, | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| @ -589,6 +603,17 @@ describe(AlbumService.name, () => { | ||||
|       expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not allow a shared user with viewer access to add assets', async () => { | ||||
|       accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set([])); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); | ||||
| 
 | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.user2, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
| 
 | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should allow a shared link user to add assets', async () => { | ||||
|       accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); | ||||
|       accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||
| @ -709,7 +734,7 @@ describe(AlbumService.name, () => { | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should skip assets without user permission to remove', async () => { | ||||
|     it('should skip assets when user has remove permission on album but not on asset', async () => { | ||||
|       accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|       albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id'])); | ||||
|  | ||||
| @ -14,10 +14,11 @@ import { | ||||
| } from 'src/dtos/album.dto'; | ||||
| import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AlbumUserEntity, AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { AlbumEntity } from 'src/entities/album.entity'; | ||||
| import { AssetEntity } from 'src/entities/asset.entity'; | ||||
| import { UserEntity } from 'src/entities/user.entity'; | ||||
| import { IAccessRepository } from 'src/interfaces/access.interface'; | ||||
| import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; | ||||
| import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; | ||||
| import { IAssetRepository } from 'src/interfaces/asset.interface'; | ||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | ||||
| @ -31,6 +32,7 @@ export class AlbumService { | ||||
|     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, | ||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||
|     @Inject(IUserRepository) private userRepository: IUserRepository, | ||||
|     @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, | ||||
|   ) { | ||||
|     this.access = AccessCore.create(accessRepository); | ||||
|   } | ||||
| @ -126,7 +128,7 @@ export class AlbumService { | ||||
|       ownerId: auth.user.id, | ||||
|       albumName: dto.albumName, | ||||
|       description: dto.description, | ||||
|       sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [], | ||||
|       albumUsers: dto.sharedWithUserIds?.map((userId) => ({ user: { id: userId } }) as AlbumUserEntity) ?? [], | ||||
|       assets, | ||||
|       albumThumbnailAssetId: assets[0]?.id || null, | ||||
|     }); | ||||
| @ -167,7 +169,7 @@ export class AlbumService { | ||||
| 
 | ||||
|   async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { | ||||
|     const album = await this.findOrFail(id, { withAssets: false }); | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_READ, id); | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id); | ||||
| 
 | ||||
|     const results = await addAssets( | ||||
|       auth, | ||||
| @ -190,7 +192,7 @@ export class AlbumService { | ||||
|   async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { | ||||
|     const album = await this.findOrFail(id, { withAssets: false }); | ||||
| 
 | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_READ, id); | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id); | ||||
| 
 | ||||
|     const results = await removeAssets( | ||||
|       auth, | ||||
| @ -209,17 +211,25 @@ export class AlbumService { | ||||
|     return results; | ||||
|   } | ||||
| 
 | ||||
|   async addUsers(auth: AuthDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> { | ||||
|   async addUsers(auth: AuthDto, id: string, { albumUsers, sharedUserIds }: AddUsersDto): Promise<AlbumResponseDto> { | ||||
|     // Remove once deprecated sharedUserIds is removed
 | ||||
|     if (!albumUsers) { | ||||
|       if (!sharedUserIds) { | ||||
|         throw new BadRequestException('No users provided'); | ||||
|       } | ||||
|       albumUsers = sharedUserIds.map((userId) => ({ userId, role: AlbumUserRole.EDITOR })); | ||||
|     } | ||||
| 
 | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); | ||||
| 
 | ||||
|     const album = await this.findOrFail(id, { withAssets: false }); | ||||
| 
 | ||||
|     for (const userId of dto.sharedUserIds) { | ||||
|     for (const { userId, role } of albumUsers) { | ||||
|       if (album.ownerId === userId) { | ||||
|         throw new BadRequestException('Cannot be shared with owner'); | ||||
|       } | ||||
| 
 | ||||
|       const exists = album.sharedUsers.find((user) => user.id === userId); | ||||
|       const exists = album.albumUsers.find(({ user: { id } }) => id === userId); | ||||
|       if (exists) { | ||||
|         throw new BadRequestException('User already added'); | ||||
|       } | ||||
| @ -229,16 +239,10 @@ export class AlbumService { | ||||
|         throw new BadRequestException('User not found'); | ||||
|       } | ||||
| 
 | ||||
|       album.sharedUsers.push({ id: userId } as UserEntity); | ||||
|       await this.albumUserRepository.create({ userId: userId, albumId: id, role }); | ||||
|     } | ||||
| 
 | ||||
|     return this.albumRepository | ||||
|       .update({ | ||||
|         id: album.id, | ||||
|         updatedAt: new Date(), | ||||
|         sharedUsers: album.sharedUsers, | ||||
|       }) | ||||
|       .then(mapAlbumWithoutAssets); | ||||
|     return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); | ||||
|   } | ||||
| 
 | ||||
|   async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> { | ||||
| @ -252,7 +256,7 @@ export class AlbumService { | ||||
|       throw new BadRequestException('Cannot remove album owner'); | ||||
|     } | ||||
| 
 | ||||
|     const exists = album.sharedUsers.find((user) => user.id === userId); | ||||
|     const exists = album.albumUsers.find(({ user: { id } }) => id === userId); | ||||
|     if (!exists) { | ||||
|       throw new BadRequestException('Album not shared with user'); | ||||
|     } | ||||
| @ -262,11 +266,13 @@ export class AlbumService { | ||||
|       await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); | ||||
|     } | ||||
| 
 | ||||
|     await this.albumRepository.update({ | ||||
|       id: album.id, | ||||
|       updatedAt: new Date(), | ||||
|       sharedUsers: album.sharedUsers.filter((user) => user.id !== userId), | ||||
|     }); | ||||
|     await this.albumUserRepository.delete({ albumId: id, userId }); | ||||
|   } | ||||
| 
 | ||||
|   async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> { | ||||
|     await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); | ||||
| 
 | ||||
|     await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); | ||||
|   } | ||||
| 
 | ||||
|   private async findOrFail(id: string, options: AlbumInfoOptions) { | ||||
|  | ||||
							
								
								
									
										52
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										52
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,4 @@ | ||||
| import { AlbumUserRole } from 'src/entities/album-user.entity'; | ||||
| import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| import { authStub } from 'test/fixtures/auth.stub'; | ||||
| @ -17,7 +18,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -34,7 +35,15 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.user1], | ||||
|     albumUsers: [ | ||||
|       { | ||||
|         user: userStub.user1, | ||||
|         album: undefined as unknown as AlbumEntity, | ||||
|         role: AlbumUserRole.EDITOR, | ||||
|         userId: userStub.user1.id, | ||||
|         albumId: 'album-2', | ||||
|       }, | ||||
|     ], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -51,7 +60,22 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.user1, userStub.user2], | ||||
|     albumUsers: [ | ||||
|       { | ||||
|         user: userStub.user1, | ||||
|         album: undefined as unknown as AlbumEntity, | ||||
|         role: AlbumUserRole.EDITOR, | ||||
|         userId: userStub.user1.id, | ||||
|         albumId: 'album-3', | ||||
|       }, | ||||
|       { | ||||
|         user: userStub.user2, | ||||
|         album: undefined as unknown as AlbumEntity, | ||||
|         role: AlbumUserRole.EDITOR, | ||||
|         userId: userStub.user2.id, | ||||
|         albumId: 'album-3', | ||||
|       }, | ||||
|     ], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -68,7 +92,15 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.admin], | ||||
|     albumUsers: [ | ||||
|       { | ||||
|         user: userStub.admin, | ||||
|         album: undefined as unknown as AlbumEntity, | ||||
|         role: AlbumUserRole.EDITOR, | ||||
|         userId: userStub.admin.id, | ||||
|         albumId: 'album-3', | ||||
|       }, | ||||
|     ], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -85,7 +117,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -102,7 +134,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -119,7 +151,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -136,7 +168,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -153,7 +185,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| @ -170,7 +202,7 @@ export const albumStub = { | ||||
|     updatedAt: new Date(), | ||||
|     deletedAt: null, | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     albumUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|  | ||||
							
								
								
									
										2
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							| @ -467,6 +467,7 @@ export const assetStub = { | ||||
|     library: libraryStub.uploadLibrary1, | ||||
|     exifInfo: { | ||||
|       fileSizeInByte: 100_000, | ||||
|       timeZone: `America/New_York`, | ||||
|     }, | ||||
|   } as AssetEntity), | ||||
| 
 | ||||
| @ -483,6 +484,7 @@ export const assetStub = { | ||||
|     library: libraryStub.uploadLibrary1, | ||||
|     exifInfo: { | ||||
|       fileSizeInByte: 25_000, | ||||
|       timeZone: `America/New_York`, | ||||
|     }, | ||||
|   } as AssetEntity), | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							| @ -103,6 +103,7 @@ const albumResponse: AlbumResponseDto = { | ||||
|   ownerId: 'admin_id', | ||||
|   owner: mapUser(userStub.admin), | ||||
|   sharedUsers: [], | ||||
|   albumUsers: [], | ||||
|   shared: false, | ||||
|   hasSharedLink: false, | ||||
|   assets: [], | ||||
| @ -186,7 +187,7 @@ export const sharedLinkStub = { | ||||
|       deletedAt: null, | ||||
|       albumThumbnailAsset: null, | ||||
|       albumThumbnailAssetId: null, | ||||
|       sharedUsers: [], | ||||
|       albumUsers: [], | ||||
|       sharedLinks: [], | ||||
|       isActivityEnabled: true, | ||||
|       order: AssetOrder.DESC, | ||||
|  | ||||
							
								
								
									
										10
									
								
								server/test/repositories/album-user.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/test/repositories/album-user.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; | ||||
| import { Mocked } from 'vitest'; | ||||
| 
 | ||||
| export const newAlbumUserRepositoryMock = (): Mocked<IAlbumUserRepository> => { | ||||
|   return { | ||||
|     create: vitest.fn(), | ||||
|     delete: vitest.fn(), | ||||
|     update: vitest.fn(), | ||||
|   }; | ||||
| }; | ||||
| @ -1,7 +1,7 @@ | ||||
| <script lang="ts"> | ||||
|   import { onMount } from 'svelte'; | ||||
|   import { groupBy, orderBy } from 'lodash-es'; | ||||
|   import { addUsersToAlbum, deleteAlbum, type UserResponseDto, type AlbumResponseDto } from '@immich/sdk'; | ||||
|   import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto } from '@immich/sdk'; | ||||
|   import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js'; | ||||
|   import Icon from '$lib/components/elements/icon.svelte'; | ||||
|   import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte'; | ||||
| @ -328,7 +328,7 @@ | ||||
|     updateAlbumInfo(album); | ||||
|   }; | ||||
| 
 | ||||
|   const handleAddUsers = async (users: UserResponseDto[]) => { | ||||
|   const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { | ||||
|     if (!albumToShare) { | ||||
|       return; | ||||
|     } | ||||
| @ -336,7 +336,7 @@ | ||||
|       const album = await addUsersToAlbum({ | ||||
|         id: albumToShare.id, | ||||
|         addUsersDto: { | ||||
|           sharedUserIds: [...users].map(({ id }) => id), | ||||
|           albumUsers, | ||||
|         }, | ||||
|       }); | ||||
|       updateAlbumInfo(album); | ||||
|  | ||||
| @ -1,5 +1,12 @@ | ||||
| <script lang="ts"> | ||||
|   import { getMyUserInfo, removeUserFromAlbum, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk'; | ||||
|   import { | ||||
|     getMyUserInfo, | ||||
|     removeUserFromAlbum, | ||||
|     type AlbumResponseDto, | ||||
|     type UserResponseDto, | ||||
|     updateAlbumUser, | ||||
|     AlbumUserRole, | ||||
|   } from '@immich/sdk'; | ||||
|   import { mdiDotsVertical } from '@mdi/js'; | ||||
|   import { createEventDispatcher, onMount } from 'svelte'; | ||||
|   import { getContextMenuPosition } from '../../utils/context-menu'; | ||||
| @ -17,6 +24,7 @@ | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     remove: string; | ||||
|     refreshAlbum: void; | ||||
|   }>(); | ||||
| 
 | ||||
|   let currentUser: UserResponseDto; | ||||
| @ -63,6 +71,19 @@ | ||||
|       selectedRemoveUser = null; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => { | ||||
|     try { | ||||
|       await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } }); | ||||
|       const message = `Set ${user.name} as ${role}`; | ||||
|       dispatch('refreshAlbum'); | ||||
|       notificationController.show({ type: NotificationType.Info, message }); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Unable to set user role'); | ||||
|     } finally { | ||||
|       selectedRemoveUser = null; | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
| 
 | ||||
| {#if !selectedRemoveUser} | ||||
| @ -78,7 +99,7 @@ | ||||
|           <p class="text-sm">Owner</p> | ||||
|         </div> | ||||
|       </div> | ||||
|       {#each album.sharedUsers as user} | ||||
|       {#each album.albumUsers as { user, role }} | ||||
|         <div | ||||
|           class="flex w-full place-items-center justify-between gap-4 p-5 rounded-xl transition-colors hover:bg-gray-50 dark:hover:bg-gray-700" | ||||
|         > | ||||
| @ -87,7 +108,14 @@ | ||||
|             <p class="text-sm font-medium">{user.name}</p> | ||||
|           </div> | ||||
| 
 | ||||
|           <div id="icon-{user.id}" class="flex place-items-center"> | ||||
|           <div id="icon-{user.id}" class="flex place-items-center gap-2 text-sm"> | ||||
|             <div> | ||||
|               {#if role === AlbumUserRole.Viewer} | ||||
|                 Viewer | ||||
|               {:else} | ||||
|                 Editor | ||||
|               {/if} | ||||
|             </div> | ||||
|             {#if isOwned} | ||||
|               <div> | ||||
|                 <CircleIconButton | ||||
| @ -101,6 +129,14 @@ | ||||
| 
 | ||||
|                 {#if selectedMenuUser === user} | ||||
|                   <ContextMenu {...position} on:outclick={() => (selectedMenuUser = null)}> | ||||
|                     {#if role === AlbumUserRole.Viewer} | ||||
|                       <MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" /> | ||||
|                     {:else} | ||||
|                       <MenuOption | ||||
|                         on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)} | ||||
|                         text="Disallow edits" | ||||
|                       /> | ||||
|                     {/if} | ||||
|                     <MenuOption on:click={handleMenuRemove} text="Remove" /> | ||||
|                   </ContextMenu> | ||||
|                 {/if} | ||||
|  | ||||
| @ -1,27 +1,36 @@ | ||||
| <script lang="ts"> | ||||
|   import { goto } from '$app/navigation'; | ||||
|   import Dropdown from '$lib/components/elements/dropdown.svelte'; | ||||
|   import Icon from '$lib/components/elements/icon.svelte'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import { AppRoute } from '$lib/constants'; | ||||
|   import { | ||||
|     AlbumUserRole, | ||||
|     getAllSharedLinks, | ||||
|     getAllUsers, | ||||
|     type AlbumResponseDto, | ||||
|     type AlbumUserAddDto, | ||||
|     type SharedLinkResponseDto, | ||||
|     type UserResponseDto, | ||||
|   } from '@immich/sdk'; | ||||
|   import { mdiCheck, mdiLink, mdiShareCircle } from '@mdi/js'; | ||||
|   import { mdiCheck, mdiEye, mdiLink, mdiPencil, mdiShareCircle } from '@mdi/js'; | ||||
|   import { createEventDispatcher, onMount } from 'svelte'; | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|   import UserAvatar from '../shared-components/user-avatar.svelte'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
| 
 | ||||
|   export let album: AlbumResponseDto; | ||||
|   export let onClose: () => void; | ||||
|   let users: UserResponseDto[] = []; | ||||
|   let selectedUsers: UserResponseDto[] = []; | ||||
|   let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {}; | ||||
| 
 | ||||
|   const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [ | ||||
|     { title: 'Editor', value: AlbumUserRole.Editor, icon: mdiPencil }, | ||||
|     { title: 'Viewer', value: AlbumUserRole.Viewer, icon: mdiEye }, | ||||
|     { title: 'Remove', value: 'none' }, | ||||
|   ]; | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     select: UserResponseDto[]; | ||||
|     select: AlbumUserAddDto[]; | ||||
|     share: void; | ||||
|   }>(); | ||||
|   let sharedLinks: SharedLinkResponseDto[] = []; | ||||
| @ -43,71 +52,91 @@ | ||||
|     sharedLinks = data.filter((link) => link.album?.id === album.id); | ||||
|   }; | ||||
| 
 | ||||
|   const handleSelect = (user: UserResponseDto) => { | ||||
|     selectedUsers = selectedUsers.includes(user) | ||||
|       ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id) | ||||
|       : [...selectedUsers, user]; | ||||
|   const handleToggle = (user: UserResponseDto) => { | ||||
|     if (Object.keys(selectedUsers).includes(user.id)) { | ||||
|       delete selectedUsers[user.id]; | ||||
|       selectedUsers = selectedUsers; | ||||
|     } else { | ||||
|       selectedUsers[user.id] = { user, role: AlbumUserRole.Editor }; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleUnselect = (user: UserResponseDto) => { | ||||
|     selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id); | ||||
|   const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => { | ||||
|     if (role === 'none') { | ||||
|       delete selectedUsers[user.id]; | ||||
|       selectedUsers = selectedUsers; | ||||
|     } else { | ||||
|       selectedUsers[user.id].role = role; | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
| 
 | ||||
| <FullScreenModal id="user-selection-modal" title="Invite to album" showLogo {onClose}> | ||||
|   {#if selectedUsers.length > 0} | ||||
|     <div class="mb-2 flex flex-wrap place-items-center gap-4 overflow-x-auto px-5 py-2 sticky"> | ||||
|       <p class="font-medium">To</p> | ||||
| 
 | ||||
|       {#each selectedUsers as user} | ||||
|         {#key user.id} | ||||
|           <button | ||||
|             on:click={() => handleUnselect(user)} | ||||
|             class="flex place-items-center gap-1 rounded-full border border-gray-500 p-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700" | ||||
|           > | ||||
|             <UserAvatar {user} size="sm" /> | ||||
|             <p class="text-xs font-medium">{user.name}</p> | ||||
|           </button> | ||||
|         {/key} | ||||
|       {/each} | ||||
|     </div> | ||||
|   {/if} | ||||
| 
 | ||||
|   <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> | ||||
|     {#if users.length > 0} | ||||
|       <p class="text-xs font-medium">SUGGESTIONS</p> | ||||
| 
 | ||||
|       <div class="my-4"> | ||||
|         {#each users as user} | ||||
|           <button | ||||
|             on:click={() => handleSelect(user)} | ||||
|             class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl" | ||||
|           > | ||||
|             {#if selectedUsers.includes(user)} | ||||
|   {#if Object.keys(selectedUsers).length > 0} | ||||
|     <div class="mb-2 py-2 sticky"> | ||||
|       <p class="text-xs font-medium">SELECTED</p> | ||||
|       <div class="my-2"> | ||||
|         {#each Object.values(selectedUsers) as { user }} | ||||
|           {#key user.id} | ||||
|             <div class="flex place-items-center gap-4 p-4"> | ||||
|               <div | ||||
|                 class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-primary text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-primary dark:text-immich-dark-bg" | ||||
|                 class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success" | ||||
|               > | ||||
|                 <Icon path={mdiCheck} size={24} /> | ||||
|               </div> | ||||
|             {:else} | ||||
|               <UserAvatar {user} size="md" /> | ||||
|             {/if} | ||||
| 
 | ||||
|             <div class="text-left"> | ||||
|               <p class="text-immich-fg dark:text-immich-dark-fg"> | ||||
|                 {user.name} | ||||
|               </p> | ||||
|               <p class="text-xs"> | ||||
|                 {user.email} | ||||
|               </p> | ||||
|               <!-- <UserAvatar {user} size="md" /> --> | ||||
|               <div class="text-left flex-grow"> | ||||
|                 <p class="text-immich-fg dark:text-immich-dark-fg"> | ||||
|                   {user.name} | ||||
|                 </p> | ||||
|                 <p class="text-xs"> | ||||
|                   {user.email} | ||||
|                 </p> | ||||
|               </div> | ||||
| 
 | ||||
|               <Dropdown | ||||
|                 title="Role" | ||||
|                 options={roleOptions} | ||||
|                 render={({ title, icon }) => ({ title, icon })} | ||||
|                 on:select={({ detail: { value } }) => handleChangeRole(user, value)} | ||||
|               /> | ||||
|             </div> | ||||
|           </button> | ||||
|           {/key} | ||||
|         {/each} | ||||
|       </div> | ||||
|     </div> | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if users.length + Object.keys(selectedUsers).length === 0} | ||||
|     <p class="p-5 text-sm"> | ||||
|       Looks like you have shared this album with all users or you don't have any user to share with. | ||||
|     </p> | ||||
|   {/if} | ||||
| 
 | ||||
|   <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> | ||||
|     {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} | ||||
|       <p class="text-xs font-medium">SUGGESTIONS</p> | ||||
| 
 | ||||
|       <div class="my-2"> | ||||
|         {#each users as user} | ||||
|           {#if !Object.keys(selectedUsers).includes(user.id)} | ||||
|             <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"> | ||||
|               <button on:click={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4"> | ||||
|                 <UserAvatar {user} size="md" /> | ||||
|                 <div class="text-left flex-grow"> | ||||
|                   <p class="text-immich-fg dark:text-immich-dark-fg"> | ||||
|                     {user.name} | ||||
|                   </p> | ||||
|                   <p class="text-xs"> | ||||
|                     {user.email} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </button> | ||||
|             </div> | ||||
|           {/if} | ||||
|         {/each} | ||||
|       </div> | ||||
|     {:else} | ||||
|       <p class="p-5 text-sm"> | ||||
|         Looks like you have shared this album with all users or you don't have any user to share with. | ||||
|       </p> | ||||
|     {/if} | ||||
|   </div> | ||||
| 
 | ||||
| @ -117,8 +146,12 @@ | ||||
|         size="sm" | ||||
|         fullwidth | ||||
|         rounded="full" | ||||
|         disabled={selectedUsers.length === 0} | ||||
|         on:click={() => dispatch('select', selectedUsers)}>Add</Button | ||||
|         disabled={Object.keys(selectedUsers).length === 0} | ||||
|         on:click={() => | ||||
|           dispatch( | ||||
|             'select', | ||||
|             Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })), | ||||
|           )}>Add</Button | ||||
|       > | ||||
|     </div> | ||||
|   {/if} | ||||
|  | ||||
| @ -1,6 +1,9 @@ | ||||
| <script lang="ts"> | ||||
|   import { afterNavigate, goto } from '$app/navigation'; | ||||
|   import AlbumDescription from '$lib/components/album-page/album-description.svelte'; | ||||
|   import AlbumOptions from '$lib/components/album-page/album-options.svelte'; | ||||
|   import AlbumSummary from '$lib/components/album-page/album-summary.svelte'; | ||||
|   import AlbumTitle from '$lib/components/album-page/album-title.svelte'; | ||||
|   import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte'; | ||||
|   import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte'; | ||||
|   import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'; | ||||
| @ -39,12 +42,16 @@ | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||
|   import { user } from '$lib/stores/user.store'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import { downloadAlbum } from '$lib/utils/asset-utils'; | ||||
|   import { clickOutside } from '$lib/utils/click-outside'; | ||||
|   import { getContextMenuPosition } from '$lib/utils/context-menu'; | ||||
|   import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation'; | ||||
|   import { | ||||
|     AlbumUserRole, | ||||
|     AssetOrder, | ||||
|     ReactionLevel, | ||||
|     ReactionType, | ||||
|     addAssetsToAlbum, | ||||
| @ -57,29 +64,23 @@ | ||||
|     getAlbumInfo, | ||||
|     updateAlbumInfo, | ||||
|     type ActivityResponseDto, | ||||
|     type UserResponseDto, | ||||
|     AssetOrder, | ||||
|     type AlbumUserAddDto, | ||||
|   } from '@immich/sdk'; | ||||
|   import { | ||||
|     mdiArrowLeft, | ||||
|     mdiCogOutline, | ||||
|     mdiDeleteOutline, | ||||
|     mdiDotsVertical, | ||||
|     mdiFolderDownloadOutline, | ||||
|     mdiLink, | ||||
|     mdiPlus, | ||||
|     mdiShareVariantOutline, | ||||
|     mdiPresentationPlay, | ||||
|     mdiCogOutline, | ||||
|     mdiImageOutline, | ||||
|     mdiImagePlusOutline, | ||||
|     mdiLink, | ||||
|     mdiPlus, | ||||
|     mdiPresentationPlay, | ||||
|     mdiShareVariantOutline, | ||||
|   } from '@mdi/js'; | ||||
|   import { fly } from 'svelte/transition'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import AlbumTitle from '$lib/components/album-page/album-title.svelte'; | ||||
|   import AlbumDescription from '$lib/components/album-page/album-description.svelte'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import AlbumSummary from '$lib/components/album-page/album-summary.svelte'; | ||||
|   import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation'; | ||||
| 
 | ||||
|   export let data: PageData; | ||||
| 
 | ||||
| @ -137,6 +138,9 @@ | ||||
|   $: showActivityStatus = | ||||
|     album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0); | ||||
| 
 | ||||
|   $: isEditor = album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor; | ||||
|   $: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer); | ||||
| 
 | ||||
|   afterNavigate(({ from }) => { | ||||
|     let url: string | undefined = from?.url?.pathname; | ||||
| 
 | ||||
| @ -312,12 +316,12 @@ | ||||
|     viewMode = ViewMode.VIEW; | ||||
|   }; | ||||
| 
 | ||||
|   const handleAddUsers = async (users: UserResponseDto[]) => { | ||||
|   const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { | ||||
|     try { | ||||
|       album = await addUsersToAlbum({ | ||||
|         id: album.id, | ||||
|         addUsersDto: { | ||||
|           sharedUserIds: [...users].map(({ id }) => id), | ||||
|           albumUsers, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
| @ -335,9 +339,9 @@ | ||||
| 
 | ||||
|     try { | ||||
|       await refreshAlbum(); | ||||
|       viewMode = album.sharedUsers.length > 1 ? ViewMode.SELECT_USERS : ViewMode.VIEW; | ||||
|       viewMode = album.sharedUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW; | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Error deleting share users'); | ||||
|       handleError(error, 'Error deleting shared user'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| @ -433,11 +437,13 @@ | ||||
|       {#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS} | ||||
|         <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}> | ||||
|           <svelte:fragment slot="trailing"> | ||||
|             <CircleIconButton | ||||
|               title="Add photos" | ||||
|               on:click={() => (viewMode = ViewMode.SELECT_ASSETS)} | ||||
|               icon={mdiImagePlusOutline} | ||||
|             /> | ||||
|             {#if isEditor} | ||||
|               <CircleIconButton | ||||
|                 title="Add photos" | ||||
|                 on:click={() => (viewMode = ViewMode.SELECT_ASSETS)} | ||||
|                 icon={mdiImagePlusOutline} | ||||
|               /> | ||||
|             {/if} | ||||
| 
 | ||||
|             {#if isOwned} | ||||
|               <CircleIconButton | ||||
| @ -578,13 +584,25 @@ | ||||
|                       <UserAvatar user={album.owner} size="md" /> | ||||
|                     </button> | ||||
| 
 | ||||
|                     <!-- users --> | ||||
|                     {#each album.sharedUsers as user (user.id)} | ||||
|                     <!-- users with write access (collaborators) --> | ||||
|                     {#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)} | ||||
|                       <button on:click={() => (viewMode = ViewMode.VIEW_USERS)}> | ||||
|                         <UserAvatar {user} size="md" /> | ||||
|                       </button> | ||||
|                     {/each} | ||||
| 
 | ||||
|                     <!-- display ellipsis if there are readonly users too --> | ||||
|                     {#if albumHasViewers} | ||||
|                       <CircleIconButton | ||||
|                         title="View all users" | ||||
|                         backgroundColor="#d3d3d3" | ||||
|                         forceDark | ||||
|                         size="20" | ||||
|                         icon={mdiDotsVertical} | ||||
|                         on:click={() => (viewMode = ViewMode.VIEW_USERS)} | ||||
|                       /> | ||||
|                     {/if} | ||||
| 
 | ||||
|                     {#if isOwned} | ||||
|                       <CircleIconButton | ||||
|                         backgroundColor="#d3d3d3" | ||||
| @ -678,6 +696,7 @@ | ||||
|     onClose={() => (viewMode = ViewMode.VIEW)} | ||||
|     {album} | ||||
|     on:remove={({ detail: userId }) => handleRemoveUser(userId)} | ||||
|     on:refreshAlbum={refreshAlbum} | ||||
|   /> | ||||
| {/if} | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,7 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({ | ||||
|   owner: userFactory.build(), | ||||
|   shared: false, | ||||
|   sharedUsers: [], | ||||
|   albumUsers: [], | ||||
|   hasSharedLink: false, | ||||
|   isActivityEnabled: true, | ||||
|   order: AssetOrder.Desc, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user