mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05: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 {
 | 
					import {
 | 
				
			||||||
 | 
					  addAssetsToAlbum,
 | 
				
			||||||
  AlbumResponseDto,
 | 
					  AlbumResponseDto,
 | 
				
			||||||
 | 
					  AlbumUserRole,
 | 
				
			||||||
  AssetFileUploadResponseDto,
 | 
					  AssetFileUploadResponseDto,
 | 
				
			||||||
  AssetOrder,
 | 
					  AssetOrder,
 | 
				
			||||||
  LoginResponseDto,
 | 
					 | 
				
			||||||
  SharedLinkType,
 | 
					 | 
				
			||||||
  addAssetsToAlbum,
 | 
					 | 
				
			||||||
  deleteUser,
 | 
					  deleteUser,
 | 
				
			||||||
  getAlbumInfo,
 | 
					  getAlbumInfo,
 | 
				
			||||||
 | 
					  LoginResponseDto,
 | 
				
			||||||
 | 
					  SharedLinkType,
 | 
				
			||||||
} from '@immich/sdk';
 | 
					} from '@immich/sdk';
 | 
				
			||||||
import { createUserDto, uuidDto } from 'src/fixtures';
 | 
					import { createUserDto, uuidDto } from 'src/fixtures';
 | 
				
			||||||
import { errorDto } from 'src/responses';
 | 
					import { errorDto } from 'src/responses';
 | 
				
			||||||
@ -14,7 +15,8 @@ import { app, asBearerAuth, utils } from 'src/utils';
 | 
				
			|||||||
import request from 'supertest';
 | 
					import request from 'supertest';
 | 
				
			||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 | 
					import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const user1SharedUser = 'user1SharedUser';
 | 
					const user1SharedEditorUser = 'user1SharedEditorUser';
 | 
				
			||||||
 | 
					const user1SharedViewerUser = 'user1SharedViewerUser';
 | 
				
			||||||
const user1SharedLink = 'user1SharedLink';
 | 
					const user1SharedLink = 'user1SharedLink';
 | 
				
			||||||
const user1NotShared = 'user1NotShared';
 | 
					const user1NotShared = 'user1NotShared';
 | 
				
			||||||
const user2SharedUser = 'user2SharedUser';
 | 
					const user2SharedUser = 'user2SharedUser';
 | 
				
			||||||
@ -49,35 +51,61 @@ describe('/album', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const albums = await Promise.all([
 | 
					    const albums = await Promise.all([
 | 
				
			||||||
      // user 1
 | 
					      // user 1
 | 
				
			||||||
 | 
					      /* 0 */
 | 
				
			||||||
      utils.createAlbum(user1.accessToken, {
 | 
					      utils.createAlbum(user1.accessToken, {
 | 
				
			||||||
        albumName: user1SharedUser,
 | 
					        albumName: user1SharedEditorUser,
 | 
				
			||||||
        sharedWithUserIds: [user2.userId],
 | 
					        sharedWithUserIds: [user2.userId],
 | 
				
			||||||
        assetIds: [user1Asset1.id],
 | 
					        assetIds: [user1Asset1.id],
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
 | 
					      /* 1 */
 | 
				
			||||||
      utils.createAlbum(user1.accessToken, {
 | 
					      utils.createAlbum(user1.accessToken, {
 | 
				
			||||||
        albumName: user1SharedLink,
 | 
					        albumName: user1SharedLink,
 | 
				
			||||||
        assetIds: [user1Asset1.id],
 | 
					        assetIds: [user1Asset1.id],
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
 | 
					      /* 2 */
 | 
				
			||||||
      utils.createAlbum(user1.accessToken, {
 | 
					      utils.createAlbum(user1.accessToken, {
 | 
				
			||||||
        albumName: user1NotShared,
 | 
					        albumName: user1NotShared,
 | 
				
			||||||
        assetIds: [user1Asset1.id, user1Asset2.id],
 | 
					        assetIds: [user1Asset1.id, user1Asset2.id],
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // user 2
 | 
					      // user 2
 | 
				
			||||||
 | 
					      /* 3 */
 | 
				
			||||||
      utils.createAlbum(user2.accessToken, {
 | 
					      utils.createAlbum(user2.accessToken, {
 | 
				
			||||||
        albumName: user2SharedUser,
 | 
					        albumName: user2SharedUser,
 | 
				
			||||||
        sharedWithUserIds: [user1.userId],
 | 
					        sharedWithUserIds: [user1.userId, user3.userId],
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
 | 
					      /* 4 */
 | 
				
			||||||
      utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
 | 
					      utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
 | 
				
			||||||
 | 
					      /* 5 */
 | 
				
			||||||
      utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
 | 
					      utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // user 3
 | 
					      // user 3
 | 
				
			||||||
 | 
					      /* 6 */
 | 
				
			||||||
      utils.createAlbum(user3.accessToken, {
 | 
					      utils.createAlbum(user3.accessToken, {
 | 
				
			||||||
        albumName: 'Deleted',
 | 
					        albumName: 'Deleted',
 | 
				
			||||||
        sharedWithUserIds: [user1.userId],
 | 
					        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(
 | 
					    await addAssetsToAlbum(
 | 
				
			||||||
      { id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } },
 | 
					      { id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } },
 | 
				
			||||||
      { headers: asBearerAuth(user1.accessToken) },
 | 
					      { headers: asBearerAuth(user1.accessToken) },
 | 
				
			||||||
@ -85,7 +113,7 @@ describe('/album', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) });
 | 
					    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);
 | 
					    user2Albums = albums.slice(3, 6);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all([
 | 
					    await Promise.all([
 | 
				
			||||||
@ -144,7 +172,7 @@ describe('/album', () => {
 | 
				
			|||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toHaveLength(3);
 | 
					      expect(body).toHaveLength(4);
 | 
				
			||||||
      expect(body).toEqual(
 | 
					      expect(body).toEqual(
 | 
				
			||||||
        expect.arrayContaining([
 | 
					        expect.arrayContaining([
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
@ -154,7 +182,12 @@ describe('/album', () => {
 | 
				
			|||||||
          }),
 | 
					          }),
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
            ownerId: user1.userId,
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
            albumName: user1SharedUser,
 | 
					            albumName: user1SharedEditorUser,
 | 
				
			||||||
 | 
					            shared: true,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          expect.objectContaining({
 | 
				
			||||||
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
 | 
					            albumName: user1SharedViewerUser,
 | 
				
			||||||
            shared: true,
 | 
					            shared: true,
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
@ -169,12 +202,17 @@ describe('/album', () => {
 | 
				
			|||||||
    it('should return the album collection including owned and shared', async () => {
 | 
					    it('should return the album collection including owned and shared', async () => {
 | 
				
			||||||
      const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					      const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toHaveLength(3);
 | 
					      expect(body).toHaveLength(4);
 | 
				
			||||||
      expect(body).toEqual(
 | 
					      expect(body).toEqual(
 | 
				
			||||||
        expect.arrayContaining([
 | 
					        expect.arrayContaining([
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
            ownerId: user1.userId,
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
            albumName: user1SharedUser,
 | 
					            albumName: user1SharedEditorUser,
 | 
				
			||||||
 | 
					            shared: true,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          expect.objectContaining({
 | 
				
			||||||
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
 | 
					            albumName: user1SharedViewerUser,
 | 
				
			||||||
            shared: true,
 | 
					            shared: true,
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
@ -196,12 +234,17 @@ describe('/album', () => {
 | 
				
			|||||||
        .get('/album?shared=true')
 | 
					        .get('/album?shared=true')
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toHaveLength(3);
 | 
					      expect(body).toHaveLength(4);
 | 
				
			||||||
      expect(body).toEqual(
 | 
					      expect(body).toEqual(
 | 
				
			||||||
        expect.arrayContaining([
 | 
					        expect.arrayContaining([
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
            ownerId: user1.userId,
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
            albumName: user1SharedUser,
 | 
					            albumName: user1SharedEditorUser,
 | 
				
			||||||
 | 
					            shared: true,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          expect.objectContaining({
 | 
				
			||||||
 | 
					            ownerId: user1.userId,
 | 
				
			||||||
 | 
					            albumName: user1SharedViewerUser,
 | 
				
			||||||
            shared: true,
 | 
					            shared: true,
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          expect.objectContaining({
 | 
					          expect.objectContaining({
 | 
				
			||||||
@ -248,7 +291,7 @@ describe('/album', () => {
 | 
				
			|||||||
        .get(`/album?shared=true&assetId=${user1Asset1.id}`)
 | 
					        .get(`/album?shared=true&assetId=${user1Asset1.id}`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      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 () => {
 | 
					    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}`)
 | 
					        .get(`/album?shared=false&assetId=${user1Asset1.id}`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      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)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .get(`/album/${user2Albums[0].id}?withoutAssets=false`)
 | 
					        .get(`/album/${user2Albums[0].id}?withoutAssets=false`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual({
 | 
					      expect(body).toMatchObject({ id: user2Albums[0].id });
 | 
				
			||||||
        ...user2Albums[0],
 | 
					 | 
				
			||||||
        assets: [expect.objectContaining({ id: user2Albums[0].assets[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 () => {
 | 
					    it('should return album info with assets when withoutAssets is undefined', async () => {
 | 
				
			||||||
@ -330,7 +379,7 @@ describe('/album', () => {
 | 
				
			|||||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      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,
 | 
					        albumThumbnailAssetId: null,
 | 
				
			||||||
        shared: false,
 | 
					        shared: false,
 | 
				
			||||||
        sharedUsers: [],
 | 
					        sharedUsers: [],
 | 
				
			||||||
 | 
					        albumUsers: [],
 | 
				
			||||||
        hasSharedLink: false,
 | 
					        hasSharedLink: false,
 | 
				
			||||||
        assets: [],
 | 
					        assets: [],
 | 
				
			||||||
        assetCount: 0,
 | 
					        assetCount: 0,
 | 
				
			||||||
@ -395,6 +445,17 @@ describe('/album', () => {
 | 
				
			|||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
 | 
					      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', () => {
 | 
					  describe('PATCH /album/:id', () => {
 | 
				
			||||||
@ -425,6 +486,26 @@ describe('/album', () => {
 | 
				
			|||||||
        description: 'An album description',
 | 
					        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', () => {
 | 
					  describe('DELETE /album/:id/assets', () => {
 | 
				
			||||||
@ -488,6 +569,16 @@ describe('/album', () => {
 | 
				
			|||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
 | 
					      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', () => {
 | 
					  describe('PUT :id/users', () => {
 | 
				
			||||||
@ -510,7 +601,7 @@ describe('/album', () => {
 | 
				
			|||||||
      const { status, body } = await request(app)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .put(`/album/${album.id}/users`)
 | 
					        .put(`/album/${album.id}/users`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
        .send({ sharedUserIds: [user2.userId] });
 | 
					        .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(200);
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
      expect(body).toEqual(
 | 
					      expect(body).toEqual(
 | 
				
			||||||
@ -524,7 +615,7 @@ describe('/album', () => {
 | 
				
			|||||||
      const { status, body } = await request(app)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .put(`/album/${album.id}/users`)
 | 
					        .put(`/album/${album.id}/users`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
        .send({ sharedUserIds: [user1.userId] });
 | 
					        .send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(400);
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
      expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
 | 
					      expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
 | 
				
			||||||
@ -534,15 +625,54 @@ describe('/album', () => {
 | 
				
			|||||||
      await request(app)
 | 
					      await request(app)
 | 
				
			||||||
        .put(`/album/${album.id}/users`)
 | 
					        .put(`/album/${album.id}/users`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
        .send({ sharedUserIds: [user2.userId] });
 | 
					        .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const { status, body } = await request(app)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .put(`/album/${album.id}/users`)
 | 
					        .put(`/album/${album.id}/users`)
 | 
				
			||||||
        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
        .send({ sharedUserIds: [user2.userId] });
 | 
					        .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(status).toBe(400);
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
      expect(body).toEqual(errorDto.badRequest('User already added'));
 | 
					      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,
 | 
					  searchMetadata,
 | 
				
			||||||
  signUpAdmin,
 | 
					  signUpAdmin,
 | 
				
			||||||
  updateAdminOnboarding,
 | 
					  updateAdminOnboarding,
 | 
				
			||||||
 | 
					  updateAlbumUser,
 | 
				
			||||||
  updateConfig,
 | 
					  updateConfig,
 | 
				
			||||||
  validate,
 | 
					  validate,
 | 
				
			||||||
} from '@immich/sdk';
 | 
					} from '@immich/sdk';
 | 
				
			||||||
@ -286,6 +287,9 @@ export const utils = {
 | 
				
			|||||||
  createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
 | 
					  createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
 | 
				
			||||||
    createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
 | 
					    createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateAlbumUser: (accessToken: string, args: Parameters<typeof updateAlbumUser>[0]) =>
 | 
				
			||||||
 | 
					    updateAlbumUser(args, { headers: asBearerAuth(accessToken) }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createAsset: async (
 | 
					  createAsset: async (
 | 
				
			||||||
    accessToken: string,
 | 
					    accessToken: string,
 | 
				
			||||||
    dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
 | 
					    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/AlbumApi.md
 | 
				
			||||||
doc/AlbumCountResponseDto.md
 | 
					doc/AlbumCountResponseDto.md
 | 
				
			||||||
doc/AlbumResponseDto.md
 | 
					doc/AlbumResponseDto.md
 | 
				
			||||||
 | 
					doc/AlbumUserAddDto.md
 | 
				
			||||||
 | 
					doc/AlbumUserResponseDto.md
 | 
				
			||||||
 | 
					doc/AlbumUserRole.md
 | 
				
			||||||
doc/AllJobStatusResponseDto.md
 | 
					doc/AllJobStatusResponseDto.md
 | 
				
			||||||
doc/AssetApi.md
 | 
					doc/AssetApi.md
 | 
				
			||||||
doc/AssetBulkDeleteDto.md
 | 
					doc/AssetBulkDeleteDto.md
 | 
				
			||||||
@ -189,6 +192,7 @@ doc/TranscodeHWAccel.md
 | 
				
			|||||||
doc/TranscodePolicy.md
 | 
					doc/TranscodePolicy.md
 | 
				
			||||||
doc/TrashApi.md
 | 
					doc/TrashApi.md
 | 
				
			||||||
doc/UpdateAlbumDto.md
 | 
					doc/UpdateAlbumDto.md
 | 
				
			||||||
 | 
					doc/UpdateAlbumUserDto.md
 | 
				
			||||||
doc/UpdateAssetDto.md
 | 
					doc/UpdateAssetDto.md
 | 
				
			||||||
doc/UpdateLibraryDto.md
 | 
					doc/UpdateLibraryDto.md
 | 
				
			||||||
doc/UpdatePartnerDto.md
 | 
					doc/UpdatePartnerDto.md
 | 
				
			||||||
@ -249,6 +253,9 @@ lib/model/add_users_dto.dart
 | 
				
			|||||||
lib/model/admin_onboarding_update_dto.dart
 | 
					lib/model/admin_onboarding_update_dto.dart
 | 
				
			||||||
lib/model/album_count_response_dto.dart
 | 
					lib/model/album_count_response_dto.dart
 | 
				
			||||||
lib/model/album_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/all_job_status_response_dto.dart
 | 
				
			||||||
lib/model/api_key_create_dto.dart
 | 
					lib/model/api_key_create_dto.dart
 | 
				
			||||||
lib/model/api_key_create_response_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_hw_accel.dart
 | 
				
			||||||
lib/model/transcode_policy.dart
 | 
					lib/model/transcode_policy.dart
 | 
				
			||||||
lib/model/update_album_dto.dart
 | 
					lib/model/update_album_dto.dart
 | 
				
			||||||
 | 
					lib/model/update_album_user_dto.dart
 | 
				
			||||||
lib/model/update_asset_dto.dart
 | 
					lib/model/update_asset_dto.dart
 | 
				
			||||||
lib/model/update_library_dto.dart
 | 
					lib/model/update_library_dto.dart
 | 
				
			||||||
lib/model/update_partner_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_api_test.dart
 | 
				
			||||||
test/album_count_response_dto_test.dart
 | 
					test/album_count_response_dto_test.dart
 | 
				
			||||||
test/album_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/all_job_status_response_dto_test.dart
 | 
				
			||||||
test/api_key_api_test.dart
 | 
					test/api_key_api_test.dart
 | 
				
			||||||
test/api_key_create_dto_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/transcode_policy_test.dart
 | 
				
			||||||
test/trash_api_test.dart
 | 
					test/trash_api_test.dart
 | 
				
			||||||
test/update_album_dto_test.dart
 | 
					test/update_album_dto_test.dart
 | 
				
			||||||
 | 
					test/update_album_user_dto_test.dart
 | 
				
			||||||
test/update_asset_dto_test.dart
 | 
					test/update_asset_dto_test.dart
 | 
				
			||||||
test/update_library_dto_test.dart
 | 
					test/update_library_dto_test.dart
 | 
				
			||||||
test/update_partner_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* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | 
 | 
				
			||||||
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | 
 | 
					*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | 
 | 
				
			||||||
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | 
 | 
					*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* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | 
 | 
				
			||||||
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
					*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
 | 
				
			||||||
*AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | 
 | 
					*AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | 
 | 
				
			||||||
@ -238,6 +239,9 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
 | 
					 - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
 | 
				
			||||||
 - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
 | 
					 - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
 | 
				
			||||||
 - [AlbumResponseDto](doc//AlbumResponseDto.md)
 | 
					 - [AlbumResponseDto](doc//AlbumResponseDto.md)
 | 
				
			||||||
 | 
					 - [AlbumUserAddDto](doc//AlbumUserAddDto.md)
 | 
				
			||||||
 | 
					 - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md)
 | 
				
			||||||
 | 
					 - [AlbumUserRole](doc//AlbumUserRole.md)
 | 
				
			||||||
 - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
 | 
					 - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
 | 
				
			||||||
 - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
 | 
					 - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
 | 
				
			||||||
 - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
 | 
					 - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
 | 
				
			||||||
@ -388,6 +392,7 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
 | 
					 - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
 | 
				
			||||||
 - [TranscodePolicy](doc//TranscodePolicy.md)
 | 
					 - [TranscodePolicy](doc//TranscodePolicy.md)
 | 
				
			||||||
 - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
 | 
					 - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
 | 
				
			||||||
 | 
					 - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
 | 
				
			||||||
 - [UpdateAssetDto](doc//UpdateAssetDto.md)
 | 
					 - [UpdateAssetDto](doc//UpdateAssetDto.md)
 | 
				
			||||||
 - [UpdateLibraryDto](doc//UpdateLibraryDto.md)
 | 
					 - [UpdateLibraryDto](doc//UpdateLibraryDto.md)
 | 
				
			||||||
 - [UpdatePartnerDto](doc//UpdatePartnerDto.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
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					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)
 | 
					[[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 | 
 | 
					[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | 
 | 
				
			||||||
[**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | 
 | 
					[**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | 
 | 
				
			||||||
[**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | 
 | 
					[**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | 
 | 
				
			||||||
 | 
					[**updateAlbumUser**](AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/user/{userId} | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **addAssetsToAlbum**
 | 
					# **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)
 | 
					[[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** |  | 
 | 
					**albumName** | **String** |  | 
 | 
				
			||||||
**albumThumbnailAssetId** | **String** |  | 
 | 
					**albumThumbnailAssetId** | **String** |  | 
 | 
				
			||||||
 | 
					**albumUsers** | [**List<AlbumUserResponseDto>**](AlbumUserResponseDto.md) |  | [default to const []]
 | 
				
			||||||
**assetCount** | **int** |  | 
 | 
					**assetCount** | **int** |  | 
 | 
				
			||||||
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []]
 | 
					**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []]
 | 
				
			||||||
**createdAt** | [**DateTime**](DateTime.md) |  | 
 | 
					**createdAt** | [**DateTime**](DateTime.md) |  | 
 | 
				
			||||||
@ -23,7 +24,7 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**owner** | [**UserResponseDto**](UserResponseDto.md) |  | 
 | 
					**owner** | [**UserResponseDto**](UserResponseDto.md) |  | 
 | 
				
			||||||
**ownerId** | **String** |  | 
 | 
					**ownerId** | **String** |  | 
 | 
				
			||||||
**shared** | **bool** |  | 
 | 
					**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] 
 | 
					**startDate** | [**DateTime**](DateTime.md) |  | [optional] 
 | 
				
			||||||
**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
					**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/admin_onboarding_update_dto.dart';
 | 
				
			||||||
part 'model/album_count_response_dto.dart';
 | 
					part 'model/album_count_response_dto.dart';
 | 
				
			||||||
part 'model/album_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/all_job_status_response_dto.dart';
 | 
				
			||||||
part 'model/asset_bulk_delete_dto.dart';
 | 
					part 'model/asset_bulk_delete_dto.dart';
 | 
				
			||||||
part 'model/asset_bulk_update_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_hw_accel.dart';
 | 
				
			||||||
part 'model/transcode_policy.dart';
 | 
					part 'model/transcode_policy.dart';
 | 
				
			||||||
part 'model/update_album_dto.dart';
 | 
					part 'model/update_album_dto.dart';
 | 
				
			||||||
 | 
					part 'model/update_album_user_dto.dart';
 | 
				
			||||||
part 'model/update_asset_dto.dart';
 | 
					part 'model/update_asset_dto.dart';
 | 
				
			||||||
part 'model/update_library_dto.dart';
 | 
					part 'model/update_library_dto.dart';
 | 
				
			||||||
part 'model/update_partner_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;
 | 
					    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);
 | 
					          return AlbumCountResponseDto.fromJson(value);
 | 
				
			||||||
        case 'AlbumResponseDto':
 | 
					        case 'AlbumResponseDto':
 | 
				
			||||||
          return AlbumResponseDto.fromJson(value);
 | 
					          return AlbumResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'AlbumUserAddDto':
 | 
				
			||||||
 | 
					          return AlbumUserAddDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'AlbumUserResponseDto':
 | 
				
			||||||
 | 
					          return AlbumUserResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'AlbumUserRole':
 | 
				
			||||||
 | 
					          return AlbumUserRoleTypeTransformer().decode(value);
 | 
				
			||||||
        case 'AllJobStatusResponseDto':
 | 
					        case 'AllJobStatusResponseDto':
 | 
				
			||||||
          return AllJobStatusResponseDto.fromJson(value);
 | 
					          return AllJobStatusResponseDto.fromJson(value);
 | 
				
			||||||
        case 'AssetBulkDeleteDto':
 | 
					        case 'AssetBulkDeleteDto':
 | 
				
			||||||
@ -504,6 +510,8 @@ class ApiClient {
 | 
				
			|||||||
          return TranscodePolicyTypeTransformer().decode(value);
 | 
					          return TranscodePolicyTypeTransformer().decode(value);
 | 
				
			||||||
        case 'UpdateAlbumDto':
 | 
					        case 'UpdateAlbumDto':
 | 
				
			||||||
          return UpdateAlbumDto.fromJson(value);
 | 
					          return UpdateAlbumDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'UpdateAlbumUserDto':
 | 
				
			||||||
 | 
					          return UpdateAlbumUserDto.fromJson(value);
 | 
				
			||||||
        case 'UpdateAssetDto':
 | 
					        case 'UpdateAssetDto':
 | 
				
			||||||
          return UpdateAssetDto.fromJson(value);
 | 
					          return UpdateAssetDto.fromJson(value);
 | 
				
			||||||
        case 'UpdateLibraryDto':
 | 
					        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) {
 | 
					  if (value is DateTime) {
 | 
				
			||||||
    return value.toUtc().toIso8601String();
 | 
					    return value.toUtc().toIso8601String();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (value is AlbumUserRole) {
 | 
				
			||||||
 | 
					    return AlbumUserRoleTypeTransformer().encode(value).toString();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (value is AssetJobName) {
 | 
					  if (value is AssetJobName) {
 | 
				
			||||||
    return AssetJobNameTypeTransformer().encode(value).toString();
 | 
					    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 {
 | 
					class AddUsersDto {
 | 
				
			||||||
  /// Returns a new [AddUsersDto] instance.
 | 
					  /// Returns a new [AddUsersDto] instance.
 | 
				
			||||||
  AddUsersDto({
 | 
					  AddUsersDto({
 | 
				
			||||||
 | 
					    this.albumUsers = const [],
 | 
				
			||||||
    this.sharedUserIds = const [],
 | 
					    this.sharedUserIds = const [],
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AlbumUserAddDto> albumUsers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Deprecated in favor of albumUsers
 | 
				
			||||||
  List<String> sharedUserIds;
 | 
					  List<String> sharedUserIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is AddUsersDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is AddUsersDto &&
 | 
				
			||||||
 | 
					    _deepEquality.equals(other.albumUsers, albumUsers) &&
 | 
				
			||||||
    _deepEquality.equals(other.sharedUserIds, sharedUserIds);
 | 
					    _deepEquality.equals(other.sharedUserIds, sharedUserIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (albumUsers.hashCode) +
 | 
				
			||||||
    (sharedUserIds.hashCode);
 | 
					    (sharedUserIds.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'AddUsersDto[sharedUserIds=$sharedUserIds]';
 | 
					  String toString() => 'AddUsersDto[albumUsers=$albumUsers, sharedUserIds=$sharedUserIds]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'albumUsers'] = this.albumUsers;
 | 
				
			||||||
      json[r'sharedUserIds'] = this.sharedUserIds;
 | 
					      json[r'sharedUserIds'] = this.sharedUserIds;
 | 
				
			||||||
    return json;
 | 
					    return json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -44,6 +51,7 @@ class AddUsersDto {
 | 
				
			|||||||
      final json = value.cast<String, dynamic>();
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return AddUsersDto(
 | 
					      return AddUsersDto(
 | 
				
			||||||
 | 
					        albumUsers: AlbumUserAddDto.listFromJson(json[r'albumUsers']),
 | 
				
			||||||
        sharedUserIds: json[r'sharedUserIds'] is Iterable
 | 
					        sharedUserIds: json[r'sharedUserIds'] is Iterable
 | 
				
			||||||
            ? (json[r'sharedUserIds'] as Iterable).cast<String>().toList(growable: false)
 | 
					            ? (json[r'sharedUserIds'] as Iterable).cast<String>().toList(growable: false)
 | 
				
			||||||
            : const [],
 | 
					            : const [],
 | 
				
			||||||
@ -94,7 +102,7 @@ class AddUsersDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    '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({
 | 
					  AlbumResponseDto({
 | 
				
			||||||
    required this.albumName,
 | 
					    required this.albumName,
 | 
				
			||||||
    required this.albumThumbnailAssetId,
 | 
					    required this.albumThumbnailAssetId,
 | 
				
			||||||
 | 
					    this.albumUsers = const [],
 | 
				
			||||||
    required this.assetCount,
 | 
					    required this.assetCount,
 | 
				
			||||||
    this.assets = const [],
 | 
					    this.assets = const [],
 | 
				
			||||||
    required this.createdAt,
 | 
					    required this.createdAt,
 | 
				
			||||||
@ -37,6 +38,8 @@ class AlbumResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String? albumThumbnailAssetId;
 | 
					  String? albumThumbnailAssetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AlbumUserResponseDto> albumUsers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int assetCount;
 | 
					  int assetCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<AssetResponseDto> assets;
 | 
					  List<AssetResponseDto> assets;
 | 
				
			||||||
@ -81,6 +84,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  bool shared;
 | 
					  bool shared;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Deprecated in favor of albumUsers
 | 
				
			||||||
  List<UserResponseDto> sharedUsers;
 | 
					  List<UserResponseDto> sharedUsers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -97,6 +101,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
  bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
 | 
				
			||||||
    other.albumName == albumName &&
 | 
					    other.albumName == albumName &&
 | 
				
			||||||
    other.albumThumbnailAssetId == albumThumbnailAssetId &&
 | 
					    other.albumThumbnailAssetId == albumThumbnailAssetId &&
 | 
				
			||||||
 | 
					    _deepEquality.equals(other.albumUsers, albumUsers) &&
 | 
				
			||||||
    other.assetCount == assetCount &&
 | 
					    other.assetCount == assetCount &&
 | 
				
			||||||
    _deepEquality.equals(other.assets, assets) &&
 | 
					    _deepEquality.equals(other.assets, assets) &&
 | 
				
			||||||
    other.createdAt == createdAt &&
 | 
					    other.createdAt == createdAt &&
 | 
				
			||||||
@ -119,6 +124,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (albumName.hashCode) +
 | 
					    (albumName.hashCode) +
 | 
				
			||||||
    (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
 | 
					    (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
 | 
				
			||||||
 | 
					    (albumUsers.hashCode) +
 | 
				
			||||||
    (assetCount.hashCode) +
 | 
					    (assetCount.hashCode) +
 | 
				
			||||||
    (assets.hashCode) +
 | 
					    (assets.hashCode) +
 | 
				
			||||||
    (createdAt.hashCode) +
 | 
					    (createdAt.hashCode) +
 | 
				
			||||||
@ -137,7 +143,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
    (updatedAt.hashCode);
 | 
					    (updatedAt.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @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() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -147,6 +153,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
    //  json[r'albumThumbnailAssetId'] = null;
 | 
					    //  json[r'albumThumbnailAssetId'] = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					      json[r'albumUsers'] = this.albumUsers;
 | 
				
			||||||
      json[r'assetCount'] = this.assetCount;
 | 
					      json[r'assetCount'] = this.assetCount;
 | 
				
			||||||
      json[r'assets'] = this.assets;
 | 
					      json[r'assets'] = this.assets;
 | 
				
			||||||
      json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
 | 
					      json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
 | 
				
			||||||
@ -192,6 +199,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
      return AlbumResponseDto(
 | 
					      return AlbumResponseDto(
 | 
				
			||||||
        albumName: mapValueOfType<String>(json, r'albumName')!,
 | 
					        albumName: mapValueOfType<String>(json, r'albumName')!,
 | 
				
			||||||
        albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
 | 
					        albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
 | 
				
			||||||
 | 
					        albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
 | 
				
			||||||
        assetCount: mapValueOfType<int>(json, r'assetCount')!,
 | 
					        assetCount: mapValueOfType<int>(json, r'assetCount')!,
 | 
				
			||||||
        assets: AssetResponseDto.listFromJson(json[r'assets']),
 | 
					        assets: AssetResponseDto.listFromJson(json[r'assets']),
 | 
				
			||||||
        createdAt: mapDateTime(json, r'createdAt', r'')!,
 | 
					        createdAt: mapDateTime(json, r'createdAt', r'')!,
 | 
				
			||||||
@ -257,6 +265,7 @@ class AlbumResponseDto {
 | 
				
			|||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'albumName',
 | 
					    'albumName',
 | 
				
			||||||
    'albumThumbnailAssetId',
 | 
					    'albumThumbnailAssetId',
 | 
				
			||||||
 | 
					    'albumUsers',
 | 
				
			||||||
    'assetCount',
 | 
					    'assetCount',
 | 
				
			||||||
    'assets',
 | 
					    'assets',
 | 
				
			||||||
    'createdAt',
 | 
					    '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();
 | 
					  // final instance = AddUsersDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('test 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 [])
 | 
					    // List<String> sharedUserIds (default value: const [])
 | 
				
			||||||
    test('to test the property `sharedUserIds`', () async {
 | 
					    test('to test the property `sharedUserIds`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // 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
 | 
					      // 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
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<AlbumUserResponseDto> albumUsers (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `albumUsers`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // int assetCount
 | 
					    // int assetCount
 | 
				
			||||||
    test('to test the property `assetCount`', () async {
 | 
					    test('to test the property `assetCount`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
@ -91,6 +96,7 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Deprecated in favor of albumUsers
 | 
				
			||||||
    // List<UserResponseDto> sharedUsers (default value: const [])
 | 
					    // List<UserResponseDto> sharedUsers (default value: const [])
 | 
				
			||||||
    test('to test the property `sharedUsers`', () async {
 | 
					    test('to test the property `sharedUsers`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // 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": [
 | 
					        "tags": [
 | 
				
			||||||
          "Album"
 | 
					          "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": {
 | 
					    "/album/{id}/users": {
 | 
				
			||||||
@ -7251,7 +7302,15 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "AddUsersDto": {
 | 
					      "AddUsersDto": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "albumUsers": {
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/AlbumUserAddDto"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "sharedUserIds": {
 | 
					          "sharedUserIds": {
 | 
				
			||||||
 | 
					            "deprecated": true,
 | 
				
			||||||
 | 
					            "description": "Deprecated in favor of albumUsers",
 | 
				
			||||||
            "items": {
 | 
					            "items": {
 | 
				
			||||||
              "format": "uuid",
 | 
					              "format": "uuid",
 | 
				
			||||||
              "type": "string"
 | 
					              "type": "string"
 | 
				
			||||||
@ -7260,7 +7319,7 @@
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "sharedUserIds"
 | 
					          "albumUsers"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -7303,6 +7362,12 @@
 | 
				
			|||||||
            "nullable": true,
 | 
					            "nullable": true,
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "albumUsers": {
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/AlbumUserResponseDto"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "assetCount": {
 | 
					          "assetCount": {
 | 
				
			||||||
            "type": "integer"
 | 
					            "type": "integer"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -7349,6 +7414,8 @@
 | 
				
			|||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "sharedUsers": {
 | 
					          "sharedUsers": {
 | 
				
			||||||
 | 
					            "deprecated": true,
 | 
				
			||||||
 | 
					            "description": "Deprecated in favor of albumUsers",
 | 
				
			||||||
            "items": {
 | 
					            "items": {
 | 
				
			||||||
              "$ref": "#/components/schemas/UserResponseDto"
 | 
					              "$ref": "#/components/schemas/UserResponseDto"
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@ -7366,6 +7433,7 @@
 | 
				
			|||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "albumName",
 | 
					          "albumName",
 | 
				
			||||||
          "albumThumbnailAssetId",
 | 
					          "albumThumbnailAssetId",
 | 
				
			||||||
 | 
					          "albumUsers",
 | 
				
			||||||
          "assetCount",
 | 
					          "assetCount",
 | 
				
			||||||
          "assets",
 | 
					          "assets",
 | 
				
			||||||
          "createdAt",
 | 
					          "createdAt",
 | 
				
			||||||
@ -7381,6 +7449,43 @@
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "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": {
 | 
					      "AllJobStatusResponseDto": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "backgroundTask": {
 | 
					          "backgroundTask": {
 | 
				
			||||||
@ -11190,6 +11295,17 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "UpdateAlbumUserDto": {
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "role": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/AlbumUserRole"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "role"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "type": "object"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "UpdateAssetDto": {
 | 
					      "UpdateAssetDto": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "dateTimeOriginal": {
 | 
					          "dateTimeOriginal": {
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,28 @@ export type ActivityCreateDto = {
 | 
				
			|||||||
export type ActivityStatisticsResponseDto = {
 | 
					export type ActivityStatisticsResponseDto = {
 | 
				
			||||||
    comments: number;
 | 
					    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 = {
 | 
					export type ExifResponseDto = {
 | 
				
			||||||
    city?: string | null;
 | 
					    city?: string | null;
 | 
				
			||||||
    country?: string | null;
 | 
					    country?: string | null;
 | 
				
			||||||
@ -61,24 +83,6 @@ export type ExifResponseDto = {
 | 
				
			|||||||
    state?: string | null;
 | 
					    state?: string | null;
 | 
				
			||||||
    timeZone?: 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 = {
 | 
					export type AssetFaceWithoutPersonResponseDto = {
 | 
				
			||||||
    boundingBoxX1: number;
 | 
					    boundingBoxX1: number;
 | 
				
			||||||
    boundingBoxX2: number;
 | 
					    boundingBoxX2: number;
 | 
				
			||||||
@ -144,6 +148,7 @@ export type AssetResponseDto = {
 | 
				
			|||||||
export type AlbumResponseDto = {
 | 
					export type AlbumResponseDto = {
 | 
				
			||||||
    albumName: string;
 | 
					    albumName: string;
 | 
				
			||||||
    albumThumbnailAssetId: string | null;
 | 
					    albumThumbnailAssetId: string | null;
 | 
				
			||||||
 | 
					    albumUsers: AlbumUserResponseDto[];
 | 
				
			||||||
    assetCount: number;
 | 
					    assetCount: number;
 | 
				
			||||||
    assets: AssetResponseDto[];
 | 
					    assets: AssetResponseDto[];
 | 
				
			||||||
    createdAt: string;
 | 
					    createdAt: string;
 | 
				
			||||||
@ -157,6 +162,7 @@ export type AlbumResponseDto = {
 | 
				
			|||||||
    owner: UserResponseDto;
 | 
					    owner: UserResponseDto;
 | 
				
			||||||
    ownerId: string;
 | 
					    ownerId: string;
 | 
				
			||||||
    shared: boolean;
 | 
					    shared: boolean;
 | 
				
			||||||
 | 
					    /** Deprecated in favor of albumUsers */
 | 
				
			||||||
    sharedUsers: UserResponseDto[];
 | 
					    sharedUsers: UserResponseDto[];
 | 
				
			||||||
    startDate?: string;
 | 
					    startDate?: string;
 | 
				
			||||||
    updatedAt: string;
 | 
					    updatedAt: string;
 | 
				
			||||||
@ -187,8 +193,17 @@ export type BulkIdResponseDto = {
 | 
				
			|||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
    success: boolean;
 | 
					    success: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					export type UpdateAlbumUserDto = {
 | 
				
			||||||
 | 
					    role: AlbumUserRole;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type AlbumUserAddDto = {
 | 
				
			||||||
 | 
					    role?: AlbumUserRole;
 | 
				
			||||||
 | 
					    userId: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
export type AddUsersDto = {
 | 
					export type AddUsersDto = {
 | 
				
			||||||
    sharedUserIds: string[];
 | 
					    albumUsers: AlbumUserAddDto[];
 | 
				
			||||||
 | 
					    /** Deprecated in favor of albumUsers */
 | 
				
			||||||
 | 
					    sharedUserIds?: string[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export type ApiKeyResponseDto = {
 | 
					export type ApiKeyResponseDto = {
 | 
				
			||||||
    createdAt: string;
 | 
					    createdAt: string;
 | 
				
			||||||
@ -1209,6 +1224,17 @@ export function removeUserFromAlbum({ id, userId }: {
 | 
				
			|||||||
        method: "DELETE"
 | 
					        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 }: {
 | 
					export function addUsersToAlbum({ id, addUsersDto }: {
 | 
				
			||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
    addUsersDto: AddUsersDto;
 | 
					    addUsersDto: AddUsersDto;
 | 
				
			||||||
@ -2927,6 +2953,10 @@ export enum UserAvatarColor {
 | 
				
			|||||||
    Gray = "gray",
 | 
					    Gray = "gray",
 | 
				
			||||||
    Amber = "amber"
 | 
					    Amber = "amber"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					export enum AlbumUserRole {
 | 
				
			||||||
 | 
					    Editor = "editor",
 | 
				
			||||||
 | 
					    Viewer = "viewer"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
export enum UserStatus {
 | 
					export enum UserStatus {
 | 
				
			||||||
    Active = "active",
 | 
					    Active = "active",
 | 
				
			||||||
    Removing = "removing",
 | 
					    Removing = "removing",
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  CreateAlbumDto,
 | 
					  CreateAlbumDto,
 | 
				
			||||||
  GetAlbumsDto,
 | 
					  GetAlbumsDto,
 | 
				
			||||||
  UpdateAlbumDto,
 | 
					  UpdateAlbumDto,
 | 
				
			||||||
 | 
					  UpdateAlbumUserDto,
 | 
				
			||||||
} from 'src/dtos/album.dto';
 | 
					} from 'src/dtos/album.dto';
 | 
				
			||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 | 
					import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
@ -88,6 +89,16 @@ export class AlbumController {
 | 
				
			|||||||
    return this.service.addUsers(auth, id, dto);
 | 
					    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')
 | 
					  @Delete(':id/user/:userId')
 | 
				
			||||||
  removeUserFromAlbum(
 | 
					  removeUserFromAlbum(
 | 
				
			||||||
    @Auth() auth: AuthDto,
 | 
					    @Auth() auth: AuthDto,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 | 
					import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
 | 
					import { AlbumUserRole } from 'src/entities/album-user.entity';
 | 
				
			||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
					import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
				
			||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
					import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
				
			||||||
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
 | 
					import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
 | 
				
			||||||
@ -22,6 +23,7 @@ export enum Permission {
 | 
				
			|||||||
  ALBUM_READ = 'album.read',
 | 
					  ALBUM_READ = 'album.read',
 | 
				
			||||||
  ALBUM_UPDATE = 'album.update',
 | 
					  ALBUM_UPDATE = 'album.update',
 | 
				
			||||||
  ALBUM_DELETE = 'album.delete',
 | 
					  ALBUM_DELETE = 'album.delete',
 | 
				
			||||||
 | 
					  ALBUM_ADD_ASSET = 'album.addAsset',
 | 
				
			||||||
  ALBUM_REMOVE_ASSET = 'album.removeAsset',
 | 
					  ALBUM_REMOVE_ASSET = 'album.removeAsset',
 | 
				
			||||||
  ALBUM_SHARE = 'album.share',
 | 
					  ALBUM_SHARE = 'album.share',
 | 
				
			||||||
  ALBUM_DOWNLOAD = 'album.download',
 | 
					  ALBUM_DOWNLOAD = 'album.download',
 | 
				
			||||||
@ -142,6 +144,12 @@ export class AccessCore {
 | 
				
			|||||||
          : new Set();
 | 
					          : new Set();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case Permission.ALBUM_ADD_ASSET: {
 | 
				
			||||||
 | 
					        return sharedLink.allowUpload
 | 
				
			||||||
 | 
					          ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
 | 
				
			||||||
 | 
					          : new Set();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
        return new Set();
 | 
					        return new Set();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -215,7 +223,21 @@ export class AccessCore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      case Permission.ALBUM_READ: {
 | 
					      case Permission.ALBUM_READ: {
 | 
				
			||||||
        const isOwner = 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));
 | 
					        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);
 | 
					        return setUnion(isOwner, isShared);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -233,12 +255,22 @@ export class AccessCore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      case Permission.ALBUM_DOWNLOAD: {
 | 
					      case Permission.ALBUM_DOWNLOAD: {
 | 
				
			||||||
        const isOwner = 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));
 | 
					        const isShared = await this.repository.album.checkSharedAlbumAccess(
 | 
				
			||||||
 | 
					          auth.user.id,
 | 
				
			||||||
 | 
					          setDifference(ids, isOwner),
 | 
				
			||||||
 | 
					          AlbumUserRole.VIEWER,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        return setUnion(isOwner, isShared);
 | 
					        return setUnion(isOwner, isShared);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case Permission.ALBUM_REMOVE_ASSET: {
 | 
					      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: {
 | 
					      case Permission.ASSET_UPLOAD: {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
 | 
					import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 | 
					import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.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 { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
 | 
				
			||||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
					import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,10 +13,23 @@ export class AlbumInfoDto {
 | 
				
			|||||||
  withoutAssets?: boolean;
 | 
					  withoutAssets?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AlbumUserAddDto {
 | 
				
			||||||
 | 
					  @ValidateUUID()
 | 
				
			||||||
 | 
					  userId!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsEnum(AlbumUserRole)
 | 
				
			||||||
 | 
					  @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR })
 | 
				
			||||||
 | 
					  role?: AlbumUserRole;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AddUsersDto {
 | 
					export class AddUsersDto {
 | 
				
			||||||
  @ValidateUUID({ each: true })
 | 
					  @ValidateUUID({ each: true, optional: true })
 | 
				
			||||||
  @ArrayNotEmpty()
 | 
					  @ArrayNotEmpty()
 | 
				
			||||||
  sharedUserIds!: string[];
 | 
					  @ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' })
 | 
				
			||||||
 | 
					  sharedUserIds?: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ArrayNotEmpty()
 | 
				
			||||||
 | 
					  albumUsers!: AlbumUserAddDto[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CreateAlbumDto {
 | 
					export class CreateAlbumDto {
 | 
				
			||||||
@ -83,6 +98,18 @@ export class AlbumCountResponseDto {
 | 
				
			|||||||
  notShared!: number;
 | 
					  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 {
 | 
					export class AlbumResponseDto {
 | 
				
			||||||
  id!: string;
 | 
					  id!: string;
 | 
				
			||||||
  ownerId!: string;
 | 
					  ownerId!: string;
 | 
				
			||||||
@ -92,7 +119,9 @@ export class AlbumResponseDto {
 | 
				
			|||||||
  updatedAt!: Date;
 | 
					  updatedAt!: Date;
 | 
				
			||||||
  albumThumbnailAssetId!: string | null;
 | 
					  albumThumbnailAssetId!: string | null;
 | 
				
			||||||
  shared!: boolean;
 | 
					  shared!: boolean;
 | 
				
			||||||
 | 
					  @ApiProperty({ deprecated: true, description: 'Deprecated in favor of albumUsers' })
 | 
				
			||||||
  sharedUsers!: UserResponseDto[];
 | 
					  sharedUsers!: UserResponseDto[];
 | 
				
			||||||
 | 
					  albumUsers!: AlbumUserResponseDto[];
 | 
				
			||||||
  hasSharedLink!: boolean;
 | 
					  hasSharedLink!: boolean;
 | 
				
			||||||
  assets!: AssetResponseDto[];
 | 
					  assets!: AssetResponseDto[];
 | 
				
			||||||
  owner!: UserResponseDto;
 | 
					  owner!: UserResponseDto;
 | 
				
			||||||
@ -109,13 +138,21 @@ export class AlbumResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
 | 
					export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
 | 
				
			||||||
  const sharedUsers: UserResponseDto[] = [];
 | 
					  const sharedUsers: UserResponseDto[] = [];
 | 
				
			||||||
 | 
					  const albumUsers: AlbumUserResponseDto[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (entity.sharedUsers) {
 | 
					  if (entity.albumUsers) {
 | 
				
			||||||
    for (const user of entity.sharedUsers) {
 | 
					    for (const albumUser of entity.albumUsers) {
 | 
				
			||||||
      sharedUsers.push(mapUser(user));
 | 
					      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 assets = entity.assets || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const hasSharedLink = entity.sharedLinks?.length > 0;
 | 
					  const hasSharedLink = entity.sharedLinks?.length > 0;
 | 
				
			||||||
@ -138,6 +175,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
 | 
				
			|||||||
    ownerId: entity.ownerId,
 | 
					    ownerId: entity.ownerId,
 | 
				
			||||||
    owner: mapUser(entity.owner),
 | 
					    owner: mapUser(entity.owner),
 | 
				
			||||||
    sharedUsers,
 | 
					    sharedUsers,
 | 
				
			||||||
 | 
					    albumUsers: albumUsersSorted,
 | 
				
			||||||
    shared: hasSharedUser || hasSharedLink,
 | 
					    shared: hasSharedUser || hasSharedLink,
 | 
				
			||||||
    hasSharedLink,
 | 
					    hasSharedLink,
 | 
				
			||||||
    startDate,
 | 
					    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 { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
					import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
				
			||||||
import { UserEntity } from 'src/entities/user.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 })
 | 
					  @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true })
 | 
				
			||||||
  albumThumbnailAssetId!: string | null;
 | 
					  albumThumbnailAssetId!: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ManyToMany(() => UserEntity)
 | 
					  @OneToMany(() => AlbumUserEntity, ({ album }) => album, { cascade: true, onDelete: 'CASCADE' })
 | 
				
			||||||
  @JoinTable()
 | 
					  albumUsers!: AlbumUserEntity[];
 | 
				
			||||||
  sharedUsers!: UserEntity[];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ManyToMany(() => AssetEntity, (asset) => asset.albums)
 | 
					  @ManyToMany(() => AssetEntity, (asset) => asset.albums)
 | 
				
			||||||
  @JoinTable()
 | 
					  @JoinTable()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { ActivityEntity } from 'src/entities/activity.entity';
 | 
					import { ActivityEntity } from 'src/entities/activity.entity';
 | 
				
			||||||
 | 
					import { AlbumUserEntity } from 'src/entities/album-user.entity';
 | 
				
			||||||
import { AlbumEntity } from 'src/entities/album.entity';
 | 
					import { AlbumEntity } from 'src/entities/album.entity';
 | 
				
			||||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
 | 
					import { APIKeyEntity } from 'src/entities/api-key.entity';
 | 
				
			||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 | 
					import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 | 
				
			||||||
@ -25,6 +26,7 @@ import { UserEntity } from 'src/entities/user.entity';
 | 
				
			|||||||
export const entities = [
 | 
					export const entities = [
 | 
				
			||||||
  ActivityEntity,
 | 
					  ActivityEntity,
 | 
				
			||||||
  AlbumEntity,
 | 
					  AlbumEntity,
 | 
				
			||||||
 | 
					  AlbumUserEntity,
 | 
				
			||||||
  APIKeyEntity,
 | 
					  APIKeyEntity,
 | 
				
			||||||
  AssetEntity,
 | 
					  AssetEntity,
 | 
				
			||||||
  AssetStackEntity,
 | 
					  AssetStackEntity,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import { AlbumUserRole } from 'src/entities/album-user.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IAccessRepository = 'IAccessRepository';
 | 
					export const IAccessRepository = 'IAccessRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAccessRepository {
 | 
					export interface IAccessRepository {
 | 
				
			||||||
@ -20,7 +22,7 @@ export interface IAccessRepository {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  album: {
 | 
					  album: {
 | 
				
			||||||
    checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
 | 
					    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>>;
 | 
					    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"
 | 
					  "album"."id" AS "album_id"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "album"
 | 
					  "albums" "album"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id"
 | 
				
			||||||
  LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId"
 | 
				
			||||||
  AND ("sharedUsers"."deletedAt" IS NULL)
 | 
					  AND ("albumUsers"."deletedAt" IS NULL)
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
  (
 | 
					  (
 | 
				
			||||||
    "album"."id" IN ($1)
 | 
					    "album"."id" IN ($1)
 | 
				
			||||||
    AND "album"."isActivityEnabled" = true
 | 
					    AND "album"."isActivityEnabled" = true
 | 
				
			||||||
    AND (
 | 
					    AND (
 | 
				
			||||||
      "album"."ownerId" = $2
 | 
					      "album"."ownerId" = $2
 | 
				
			||||||
      OR "sharedUsers"."id" = $2
 | 
					      OR "albumUsers"."id" = $2
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
  AND ("album"."deletedAt" IS NULL)
 | 
					  AND ("album"."deletedAt" IS NULL)
 | 
				
			||||||
@ -70,10 +70,10 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity"."id" AS "AlbumEntity_id"
 | 
					  "AlbumEntity"."id" AS "AlbumEntity_id"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
  AND (
 | 
					  AND (
 | 
				
			||||||
    "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL
 | 
					    "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
  (
 | 
					  (
 | 
				
			||||||
@ -81,7 +81,16 @@ WHERE
 | 
				
			|||||||
      ("AlbumEntity"."id" IN ($1))
 | 
					      ("AlbumEntity"."id" IN ($1))
 | 
				
			||||||
      AND (
 | 
					      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 "albums_assets_assets" "album_asset" ON "album_asset"."albumsId" = "album"."id"
 | 
				
			||||||
  INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId"
 | 
					  INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId"
 | 
				
			||||||
  AND ("asset"."deletedAt" IS NULL)
 | 
					  AND ("asset"."deletedAt" IS NULL)
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id"
 | 
				
			||||||
  LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId"
 | 
				
			||||||
  AND ("sharedUsers"."deletedAt" IS NULL)
 | 
					  AND ("albumUsers"."deletedAt" IS NULL)
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
  (
 | 
					  (
 | 
				
			||||||
    array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid []
 | 
					    array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid []
 | 
				
			||||||
    AND (
 | 
					    AND (
 | 
				
			||||||
      "album"."ownerId" = $2
 | 
					      "album"."ownerId" = $2
 | 
				
			||||||
      OR "sharedUsers"."id" = $2
 | 
					      OR "albumUsers"."id" = $2
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
  AND ("album"."deletedAt" IS NULL)
 | 
					  AND ("album"."deletedAt" IS NULL)
 | 
				
			||||||
 | 
				
			|||||||
@ -32,22 +32,25 @@ FROM
 | 
				
			|||||||
      "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					      "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
					      "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
					      "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					      "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					      "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					      "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
					      "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
					      "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"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					      "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
      "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					      "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -66,10 +69,10 @@ FROM
 | 
				
			|||||||
      AND (
 | 
					      AND (
 | 
				
			||||||
        "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
 | 
					        "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 "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
      LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					      LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
      AND (
 | 
					      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 "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
 | 
				
			||||||
    WHERE
 | 
					    WHERE
 | 
				
			||||||
@ -109,32 +112,35 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes"
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
  AND (
 | 
					  AND (
 | 
				
			||||||
    "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
 | 
					    "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 "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
  AND (
 | 
					  AND (
 | 
				
			||||||
    "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL
 | 
					    "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
  ((("AlbumEntity"."id" IN ($1))))
 | 
					  ((("AlbumEntity"."id" IN ($1))))
 | 
				
			||||||
@ -168,32 +174,35 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
					  "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes"
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
 | 
				
			||||||
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
  AND (
 | 
					  AND (
 | 
				
			||||||
    "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
 | 
					    "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 "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
  AND (
 | 
					  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 "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"
 | 
					  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)))
 | 
					          AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4)))
 | 
				
			||||||
@ -283,22 +294,25 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
					  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
				
			||||||
  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
					  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
				
			||||||
  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
					  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
					  "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"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -329,10 +343,10 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
  AND (
 | 
					  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 "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"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
@ -357,22 +371,25 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
					  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
				
			||||||
  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
					  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
				
			||||||
  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
					  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
 | 
					  "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaUsageInBytes",
 | 
					  "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"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -403,10 +420,10 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
 | 
				
			||||||
  LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
 | 
					  LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
 | 
				
			||||||
  AND (
 | 
					  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 "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"
 | 
					  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 (
 | 
					                NOT (
 | 
				
			||||||
                  "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL
 | 
					                  "AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -468,22 +487,9 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
					  "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
 | 
				
			||||||
  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
					  "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
 | 
				
			||||||
  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
					  "AlbumEntity"."order" AS "AlbumEntity_order",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
 | 
					  "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
 | 
				
			||||||
  "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_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
 | 
				
			||||||
  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
					  "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
 | 
				
			||||||
@ -514,11 +520,7 @@ SELECT
 | 
				
			|||||||
  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
					  "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
 | 
				
			||||||
FROM
 | 
					FROM
 | 
				
			||||||
  "albums" "AlbumEntity"
 | 
					  "albums" "AlbumEntity"
 | 
				
			||||||
  LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
 | 
					  LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."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 "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "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"
 | 
					  LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
 | 
				
			||||||
  AND (
 | 
					  AND (
 | 
				
			||||||
@ -531,7 +533,7 @@ WHERE
 | 
				
			|||||||
      AND (
 | 
					      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 { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 | 
					import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 | 
				
			||||||
import { ActivityEntity } from 'src/entities/activity.entity';
 | 
					import { ActivityEntity } from 'src/entities/activity.entity';
 | 
				
			||||||
 | 
					import { AlbumUserRole } from 'src/entities/album-user.entity';
 | 
				
			||||||
import { AlbumEntity } from 'src/entities/album.entity';
 | 
					import { AlbumEntity } from 'src/entities/album.entity';
 | 
				
			||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 | 
					import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
@ -81,12 +82,13 @@ class ActivityAccess implements IActivityAccess {
 | 
				
			|||||||
    return this.albumRepository
 | 
					    return this.albumRepository
 | 
				
			||||||
      .createQueryBuilder('album')
 | 
					      .createQueryBuilder('album')
 | 
				
			||||||
      .select('album.id')
 | 
					      .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] })
 | 
					      .where('album.id IN (:...albumIds)', { albumIds: [...albumIds] })
 | 
				
			||||||
      .andWhere('album.isActivityEnabled = true')
 | 
					      .andWhere('album.isActivityEnabled = true')
 | 
				
			||||||
      .andWhere(
 | 
					      .andWhere(
 | 
				
			||||||
        new Brackets((qb) => {
 | 
					        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()
 | 
					      .getMany()
 | 
				
			||||||
@ -120,7 +122,7 @@ class AlbumAccess implements IAlbumAccess {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
 | 
					  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
 | 
				
			||||||
  @ChunkedSet({ paramIndex: 1 })
 | 
					  @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) {
 | 
					    if (albumIds.size === 0) {
 | 
				
			||||||
      return new Set();
 | 
					      return new Set();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -130,8 +132,11 @@ class AlbumAccess implements IAlbumAccess {
 | 
				
			|||||||
        select: { id: true },
 | 
					        select: { id: true },
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          id: In([...albumIds]),
 | 
					          id: In([...albumIds]),
 | 
				
			||||||
          sharedUsers: {
 | 
					          albumUsers: {
 | 
				
			||||||
            id: userId,
 | 
					            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
 | 
					    return this.albumRepository
 | 
				
			||||||
      .createQueryBuilder('album')
 | 
					      .createQueryBuilder('album')
 | 
				
			||||||
      .innerJoin('album.assets', 'asset')
 | 
					      .innerJoin('album.assets', 'asset')
 | 
				
			||||||
      .leftJoin('album.sharedUsers', 'sharedUsers')
 | 
					      .leftJoin('album.albumUsers', 'album_albumUsers_users')
 | 
				
			||||||
 | 
					      .leftJoin('album_albumUsers_users.user', 'albumUsers')
 | 
				
			||||||
      .select('asset.id', 'assetId')
 | 
					      .select('asset.id', 'assetId')
 | 
				
			||||||
      .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId')
 | 
					      .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId')
 | 
				
			||||||
      .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', {
 | 
					      .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', {
 | 
				
			||||||
@ -185,7 +191,7 @@ class AssetAccess implements IAssetAccess {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
      .andWhere(
 | 
					      .andWhere(
 | 
				
			||||||
        new Brackets((qb) => {
 | 
					        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()
 | 
					      .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 { setUnion } from 'src/utils/set';
 | 
				
			||||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
 | 
					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()
 | 
					@Instrumentation()
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AlbumRepository implements IAlbumRepository {
 | 
					export class AlbumRepository implements IAlbumRepository {
 | 
				
			||||||
@ -20,10 +27,10 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID, {}] })
 | 
					  @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> = {
 | 
					    const relations: FindOptionsRelations<AlbumEntity> = {
 | 
				
			||||||
      owner: true,
 | 
					      owner: true,
 | 
				
			||||||
      sharedUsers: true,
 | 
					      albumUsers: { user: true },
 | 
				
			||||||
      assets: false,
 | 
					      assets: false,
 | 
				
			||||||
      sharedLinks: true,
 | 
					      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]] })
 | 
					  @GenerateSql({ params: [[DummyValue.UUID]] })
 | 
				
			||||||
  @ChunkedArray()
 | 
					  @ChunkedArray()
 | 
				
			||||||
  getByIds(ids: string[]): Promise<AlbumEntity[]> {
 | 
					  async getByIds(ids: string[]): Promise<AlbumEntity[]> {
 | 
				
			||||||
    return this.repository.find({
 | 
					    const albums = await this.repository.find({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        id: In(ids),
 | 
					        id: In(ids),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      relations: {
 | 
					      relations: {
 | 
				
			||||||
        owner: true,
 | 
					        owner: true,
 | 
				
			||||||
        sharedUsers: true,
 | 
					        albumUsers: { user: true },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return albums.map((album) => withoutDeletedUsers(album));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
 | 
					  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
 | 
				
			||||||
  getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
 | 
					  async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
 | 
				
			||||||
    return this.repository.find({
 | 
					    const albums = await this.repository.find({
 | 
				
			||||||
      where: [
 | 
					      where: [
 | 
				
			||||||
        { ownerId, assets: { id: assetId } },
 | 
					        { 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' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return albums.map((album) => withoutDeletedUsers(album));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @GenerateSql({ params: [[DummyValue.UUID]] })
 | 
					  @GenerateSql({ params: [[DummyValue.UUID]] })
 | 
				
			||||||
@ -127,40 +139,46 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID] })
 | 
					  @GenerateSql({ params: [DummyValue.UUID] })
 | 
				
			||||||
  getOwned(ownerId: string): Promise<AlbumEntity[]> {
 | 
					  async getOwned(ownerId: string): Promise<AlbumEntity[]> {
 | 
				
			||||||
    return this.repository.find({
 | 
					    const albums = await this.repository.find({
 | 
				
			||||||
      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
					      relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
 | 
				
			||||||
      where: { ownerId },
 | 
					      where: { ownerId },
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return albums.map((album) => withoutDeletedUsers(album));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get albums shared with and shared by owner.
 | 
					   * Get albums shared with and shared by owner.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID] })
 | 
					  @GenerateSql({ params: [DummyValue.UUID] })
 | 
				
			||||||
  getShared(ownerId: string): Promise<AlbumEntity[]> {
 | 
					  async getShared(ownerId: string): Promise<AlbumEntity[]> {
 | 
				
			||||||
    return this.repository.find({
 | 
					    const albums = await this.repository.find({
 | 
				
			||||||
      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
					      relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
 | 
				
			||||||
      where: [
 | 
					      where: [
 | 
				
			||||||
        { sharedUsers: { id: ownerId } },
 | 
					        { albumUsers: { userId: ownerId } },
 | 
				
			||||||
        { sharedLinks: { userId: ownerId } },
 | 
					        { sharedLinks: { userId: ownerId } },
 | 
				
			||||||
        { ownerId, sharedUsers: { id: Not(IsNull()) } },
 | 
					        { ownerId, albumUsers: { user: Not(IsNull()) } },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return albums.map((album) => withoutDeletedUsers(album));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get albums of owner that are _not_ shared
 | 
					   * Get albums of owner that are _not_ shared
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  @GenerateSql({ params: [DummyValue.UUID] })
 | 
					  @GenerateSql({ params: [DummyValue.UUID] })
 | 
				
			||||||
  getNotShared(ownerId: string): Promise<AlbumEntity[]> {
 | 
					  async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
 | 
				
			||||||
    return this.repository.find({
 | 
					    const albums = await this.repository.find({
 | 
				
			||||||
      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
					      relations: { albumUsers: true, sharedLinks: true, owner: true },
 | 
				
			||||||
      where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
 | 
					      where: { ownerId, albumUsers: { user: IsNull() }, sharedLinks: { id: IsNull() } },
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return albums.map((album) => withoutDeletedUsers(album));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async restoreAll(userId: string): Promise<void> {
 | 
					  async restoreAll(userId: string): Promise<void> {
 | 
				
			||||||
@ -282,7 +300,7 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
      where: { id },
 | 
					      where: { id },
 | 
				
			||||||
      relations: {
 | 
					      relations: {
 | 
				
			||||||
        owner: true,
 | 
					        owner: true,
 | 
				
			||||||
        sharedUsers: true,
 | 
					        albumUsers: { user: true },
 | 
				
			||||||
        sharedLinks: true,
 | 
					        sharedLinks: true,
 | 
				
			||||||
        assets: true,
 | 
					        assets: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
					import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
				
			||||||
import { IActivityRepository } from 'src/interfaces/activity.interface';
 | 
					import { IActivityRepository } from 'src/interfaces/activity.interface';
 | 
				
			||||||
 | 
					import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
 | 
				
			||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
					import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
 | 
					import { IKeyRepository } from 'src/interfaces/api-key.interface';
 | 
				
			||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { AccessRepository } from 'src/repositories/access.repository';
 | 
					import { AccessRepository } from 'src/repositories/access.repository';
 | 
				
			||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
 | 
					import { ActivityRepository } from 'src/repositories/activity.repository';
 | 
				
			||||||
 | 
					import { AlbumUserRepository } from 'src/repositories/album-user.repository';
 | 
				
			||||||
import { AlbumRepository } from 'src/repositories/album.repository';
 | 
					import { AlbumRepository } from 'src/repositories/album.repository';
 | 
				
			||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
 | 
					import { ApiKeyRepository } from 'src/repositories/api-key.repository';
 | 
				
			||||||
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
 | 
					import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
 | 
				
			||||||
@ -65,6 +67,7 @@ export const repositories = [
 | 
				
			|||||||
  { provide: IActivityRepository, useClass: ActivityRepository },
 | 
					  { provide: IActivityRepository, useClass: ActivityRepository },
 | 
				
			||||||
  { provide: IAccessRepository, useClass: AccessRepository },
 | 
					  { provide: IAccessRepository, useClass: AccessRepository },
 | 
				
			||||||
  { provide: IAlbumRepository, useClass: AlbumRepository },
 | 
					  { provide: IAlbumRepository, useClass: AlbumRepository },
 | 
				
			||||||
 | 
					  { provide: IAlbumUserRepository, useClass: AlbumUserRepository },
 | 
				
			||||||
  { provide: IAssetRepository, useClass: AssetRepository },
 | 
					  { provide: IAssetRepository, useClass: AssetRepository },
 | 
				
			||||||
  { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
 | 
					  { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
 | 
				
			||||||
  { provide: IAssetStackRepository, useClass: AssetStackRepository },
 | 
					  { provide: IAssetStackRepository, useClass: AssetStackRepository },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import { BadRequestException } from '@nestjs/common';
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
import _ from 'lodash';
 | 
					import _ from 'lodash';
 | 
				
			||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 | 
					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 { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.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 { authStub } from 'test/fixtures/auth.stub';
 | 
				
			||||||
import { userStub } from 'test/fixtures/user.stub';
 | 
					import { userStub } from 'test/fixtures/user.stub';
 | 
				
			||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
 | 
					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 { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
 | 
				
			||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
					import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
				
			||||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
					import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
@ -20,14 +23,16 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
  let albumMock: Mocked<IAlbumRepository>;
 | 
					  let albumMock: Mocked<IAlbumRepository>;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let userMock: Mocked<IUserRepository>;
 | 
					  let userMock: Mocked<IUserRepository>;
 | 
				
			||||||
 | 
					  let albumUserMock: Mocked<IAlbumUserRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    accessMock = newAccessRepositoryMock();
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
 | 
					    albumUserMock = newAlbumUserRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new AlbumService(accessMock, albumMock, assetMock, userMock);
 | 
					    sut = new AlbumService(accessMock, albumMock, assetMock, userMock, albumUserMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
@ -189,7 +194,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
        ownerId: authStub.admin.user.id,
 | 
					        ownerId: authStub.admin.user.id,
 | 
				
			||||||
        albumName: albumStub.empty.albumName,
 | 
					        albumName: albumStub.empty.albumName,
 | 
				
			||||||
        description: albumStub.empty.description,
 | 
					        description: albumStub.empty.description,
 | 
				
			||||||
        sharedUsers: [{ id: 'user-id' }],
 | 
					        albumUsers: [{ user: { id: 'user-id' } }],
 | 
				
			||||||
        assets: [{ id: '123' }],
 | 
					        assets: [{ id: '123' }],
 | 
				
			||||||
        albumThumbnailAssetId: '123',
 | 
					        albumThumbnailAssetId: '123',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -225,7 +230,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
        ownerId: authStub.admin.user.id,
 | 
					        ownerId: authStub.admin.user.id,
 | 
				
			||||||
        albumName: 'Test album',
 | 
					        albumName: 'Test album',
 | 
				
			||||||
        description: '',
 | 
					        description: '',
 | 
				
			||||||
        sharedUsers: [],
 | 
					        albumUsers: [],
 | 
				
			||||||
        assets: [{ id: 'asset-1' }],
 | 
					        assets: [{ id: 'asset-1' }],
 | 
				
			||||||
        albumThumbnailAssetId: 'asset-1',
 | 
					        albumThumbnailAssetId: 'asset-1',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -327,7 +332,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
  describe('addUsers', () => {
 | 
					  describe('addUsers', () => {
 | 
				
			||||||
    it('should throw an error if the auth user is not the owner', async () => {
 | 
					    it('should throw an error if the auth user is not the owner', async () => {
 | 
				
			||||||
      await expect(
 | 
					      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);
 | 
					      ).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
      expect(albumMock.update).not.toHaveBeenCalled();
 | 
					      expect(albumMock.update).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -336,7 +341,9 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
 | 
					      accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
 | 
				
			||||||
      albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
					      albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
				
			||||||
      await expect(
 | 
					      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);
 | 
					      ).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
      expect(albumMock.update).not.toHaveBeenCalled();
 | 
					      expect(albumMock.update).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -346,7 +353,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
					      albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
				
			||||||
      userMock.get.mockResolvedValue(null);
 | 
					      userMock.get.mockResolvedValue(null);
 | 
				
			||||||
      await expect(
 | 
					      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);
 | 
					      ).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
      expect(albumMock.update).not.toHaveBeenCalled();
 | 
					      expect(albumMock.update).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -356,11 +363,19 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
 | 
					      albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
 | 
				
			||||||
      albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
					      albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
 | 
				
			||||||
      userMock.get.mockResolvedValue(userStub.user2);
 | 
					      userMock.get.mockResolvedValue(userStub.user2);
 | 
				
			||||||
      await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.user.id] });
 | 
					      albumUserMock.create.mockResolvedValue({
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledWith({
 | 
					        userId: userStub.user2.id,
 | 
				
			||||||
        id: albumStub.sharedWithAdmin.id,
 | 
					        user: userStub.user2,
 | 
				
			||||||
        updatedAt: expect.any(Date),
 | 
					        albumId: albumStub.sharedWithAdmin.id,
 | 
				
			||||||
        sharedUsers: [userStub.admin, { id: authStub.user2.user.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),
 | 
					        sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id),
 | 
				
			||||||
      ).resolves.toBeUndefined();
 | 
					      ).resolves.toBeUndefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledTimes(1);
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledWith({
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledWith({
 | 
				
			||||||
        id: albumStub.sharedWithUser.id,
 | 
					        albumId: albumStub.sharedWithUser.id,
 | 
				
			||||||
        updatedAt: expect.any(Date),
 | 
					        userId: userStub.user1.id,
 | 
				
			||||||
        sharedUsers: [],
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false });
 | 
					      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),
 | 
					        sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id),
 | 
				
			||||||
      ).rejects.toBeInstanceOf(BadRequestException);
 | 
					      ).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(albumMock.update).not.toHaveBeenCalled();
 | 
					      expect(albumUserMock.delete).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
 | 
					      expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
 | 
				
			||||||
        authStub.user1.user.id,
 | 
					        authStub.user1.user.id,
 | 
				
			||||||
        new Set([albumStub.sharedWithMultiple.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);
 | 
					      await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledTimes(1);
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledWith({
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledWith({
 | 
				
			||||||
        id: albumStub.sharedWithUser.id,
 | 
					        albumId: albumStub.sharedWithUser.id,
 | 
				
			||||||
        updatedAt: expect.any(Date),
 | 
					        userId: authStub.user1.user.id,
 | 
				
			||||||
        sharedUsers: [],
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -422,11 +435,10 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
 | 
					      await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledTimes(1);
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
 | 
				
			||||||
      expect(albumMock.update).toHaveBeenCalledWith({
 | 
					      expect(albumUserMock.delete).toHaveBeenCalledWith({
 | 
				
			||||||
        id: albumStub.sharedWithUser.id,
 | 
					        albumId: albumStub.sharedWithUser.id,
 | 
				
			||||||
        updatedAt: expect.any(Date),
 | 
					        userId: authStub.user1.user.id,
 | 
				
			||||||
        sharedUsers: [],
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -512,6 +524,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
 | 
					      expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
 | 
				
			||||||
        authStub.user1.user.id,
 | 
					        authStub.user1.user.id,
 | 
				
			||||||
        new Set(['album-123']),
 | 
					        new Set(['album-123']),
 | 
				
			||||||
 | 
					        AlbumUserRole.VIEWER,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -522,6 +535,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
 | 
					      expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
 | 
				
			||||||
        authStub.admin.user.id,
 | 
					        authStub.admin.user.id,
 | 
				
			||||||
        new Set(['album-123']),
 | 
					        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']);
 | 
					      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 () => {
 | 
					    it('should allow a shared link user to add assets', async () => {
 | 
				
			||||||
      accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
 | 
					      accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
 | 
				
			||||||
      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
 | 
					      accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
 | 
				
			||||||
@ -709,7 +734,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
      expect(albumMock.update).not.toHaveBeenCalled();
 | 
					      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']));
 | 
					      accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
 | 
				
			||||||
      albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
 | 
					      albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
 | 
				
			||||||
      albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
 | 
					      albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
 | 
				
			||||||
 | 
				
			|||||||
@ -14,10 +14,11 @@ import {
 | 
				
			|||||||
} from 'src/dtos/album.dto';
 | 
					} from 'src/dtos/album.dto';
 | 
				
			||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 | 
					import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.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 { AlbumEntity } from 'src/entities/album.entity';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
import { UserEntity } from 'src/entities/user.entity';
 | 
					 | 
				
			||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
					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 { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.interface';
 | 
					import { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
@ -31,6 +32,7 @@ export class AlbumService {
 | 
				
			|||||||
    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
					    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
 | 
					    @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.access = AccessCore.create(accessRepository);
 | 
					    this.access = AccessCore.create(accessRepository);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -126,7 +128,7 @@ export class AlbumService {
 | 
				
			|||||||
      ownerId: auth.user.id,
 | 
					      ownerId: auth.user.id,
 | 
				
			||||||
      albumName: dto.albumName,
 | 
					      albumName: dto.albumName,
 | 
				
			||||||
      description: dto.description,
 | 
					      description: dto.description,
 | 
				
			||||||
      sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [],
 | 
					      albumUsers: dto.sharedWithUserIds?.map((userId) => ({ user: { id: userId } }) as AlbumUserEntity) ?? [],
 | 
				
			||||||
      assets,
 | 
					      assets,
 | 
				
			||||||
      albumThumbnailAssetId: assets[0]?.id || null,
 | 
					      albumThumbnailAssetId: assets[0]?.id || null,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -167,7 +169,7 @@ export class AlbumService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
 | 
					  async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
 | 
				
			||||||
    const album = await this.findOrFail(id, { withAssets: false });
 | 
					    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(
 | 
					    const results = await addAssets(
 | 
				
			||||||
      auth,
 | 
					      auth,
 | 
				
			||||||
@ -190,7 +192,7 @@ export class AlbumService {
 | 
				
			|||||||
  async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
 | 
					  async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
 | 
				
			||||||
    const album = await this.findOrFail(id, { withAssets: false });
 | 
					    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(
 | 
					    const results = await removeAssets(
 | 
				
			||||||
      auth,
 | 
					      auth,
 | 
				
			||||||
@ -209,17 +211,25 @@ export class AlbumService {
 | 
				
			|||||||
    return results;
 | 
					    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);
 | 
					    await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const album = await this.findOrFail(id, { withAssets: false });
 | 
					    const album = await this.findOrFail(id, { withAssets: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const userId of dto.sharedUserIds) {
 | 
					    for (const { userId, role } of albumUsers) {
 | 
				
			||||||
      if (album.ownerId === userId) {
 | 
					      if (album.ownerId === userId) {
 | 
				
			||||||
        throw new BadRequestException('Cannot be shared with owner');
 | 
					        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) {
 | 
					      if (exists) {
 | 
				
			||||||
        throw new BadRequestException('User already added');
 | 
					        throw new BadRequestException('User already added');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -229,16 +239,10 @@ export class AlbumService {
 | 
				
			|||||||
        throw new BadRequestException('User not found');
 | 
					        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
 | 
					    return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
 | 
				
			||||||
      .update({
 | 
					 | 
				
			||||||
        id: album.id,
 | 
					 | 
				
			||||||
        updatedAt: new Date(),
 | 
					 | 
				
			||||||
        sharedUsers: album.sharedUsers,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .then(mapAlbumWithoutAssets);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
 | 
					  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');
 | 
					      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) {
 | 
					    if (!exists) {
 | 
				
			||||||
      throw new BadRequestException('Album not shared with user');
 | 
					      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.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.albumRepository.update({
 | 
					    await this.albumUserRepository.delete({ albumId: id, userId });
 | 
				
			||||||
      id: album.id,
 | 
					  }
 | 
				
			||||||
      updatedAt: new Date(),
 | 
					
 | 
				
			||||||
      sharedUsers: album.sharedUsers.filter((user) => user.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) {
 | 
					  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 { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
					import { authStub } from 'test/fixtures/auth.stub';
 | 
				
			||||||
@ -17,7 +18,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -34,7 +35,15 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    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,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -51,7 +60,22 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    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,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -68,7 +92,15 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    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,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -85,7 +117,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -102,7 +134,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -119,7 +151,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -136,7 +168,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -153,7 +185,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    order: AssetOrder.DESC,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -170,7 +202,7 @@ export const albumStub = {
 | 
				
			|||||||
    updatedAt: new Date(),
 | 
					    updatedAt: new Date(),
 | 
				
			||||||
    deletedAt: null,
 | 
					    deletedAt: null,
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    albumUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    order: AssetOrder.DESC,
 | 
					    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,
 | 
					    library: libraryStub.uploadLibrary1,
 | 
				
			||||||
    exifInfo: {
 | 
					    exifInfo: {
 | 
				
			||||||
      fileSizeInByte: 100_000,
 | 
					      fileSizeInByte: 100_000,
 | 
				
			||||||
 | 
					      timeZone: `America/New_York`,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  } as AssetEntity),
 | 
					  } as AssetEntity),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -483,6 +484,7 @@ export const assetStub = {
 | 
				
			|||||||
    library: libraryStub.uploadLibrary1,
 | 
					    library: libraryStub.uploadLibrary1,
 | 
				
			||||||
    exifInfo: {
 | 
					    exifInfo: {
 | 
				
			||||||
      fileSizeInByte: 25_000,
 | 
					      fileSizeInByte: 25_000,
 | 
				
			||||||
 | 
					      timeZone: `America/New_York`,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  } as AssetEntity),
 | 
					  } 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',
 | 
					  ownerId: 'admin_id',
 | 
				
			||||||
  owner: mapUser(userStub.admin),
 | 
					  owner: mapUser(userStub.admin),
 | 
				
			||||||
  sharedUsers: [],
 | 
					  sharedUsers: [],
 | 
				
			||||||
 | 
					  albumUsers: [],
 | 
				
			||||||
  shared: false,
 | 
					  shared: false,
 | 
				
			||||||
  hasSharedLink: false,
 | 
					  hasSharedLink: false,
 | 
				
			||||||
  assets: [],
 | 
					  assets: [],
 | 
				
			||||||
@ -186,7 +187,7 @@ export const sharedLinkStub = {
 | 
				
			|||||||
      deletedAt: null,
 | 
					      deletedAt: null,
 | 
				
			||||||
      albumThumbnailAsset: null,
 | 
					      albumThumbnailAsset: null,
 | 
				
			||||||
      albumThumbnailAssetId: null,
 | 
					      albumThumbnailAssetId: null,
 | 
				
			||||||
      sharedUsers: [],
 | 
					      albumUsers: [],
 | 
				
			||||||
      sharedLinks: [],
 | 
					      sharedLinks: [],
 | 
				
			||||||
      isActivityEnabled: true,
 | 
					      isActivityEnabled: true,
 | 
				
			||||||
      order: AssetOrder.DESC,
 | 
					      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">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import { groupBy, orderBy } from 'lodash-es';
 | 
					  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 { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
 | 
					  import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
 | 
				
			||||||
@ -328,7 +328,7 @@
 | 
				
			|||||||
    updateAlbumInfo(album);
 | 
					    updateAlbumInfo(album);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddUsers = async (users: UserResponseDto[]) => {
 | 
					  const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
 | 
				
			||||||
    if (!albumToShare) {
 | 
					    if (!albumToShare) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -336,7 +336,7 @@
 | 
				
			|||||||
      const album = await addUsersToAlbum({
 | 
					      const album = await addUsersToAlbum({
 | 
				
			||||||
        id: albumToShare.id,
 | 
					        id: albumToShare.id,
 | 
				
			||||||
        addUsersDto: {
 | 
					        addUsersDto: {
 | 
				
			||||||
          sharedUserIds: [...users].map(({ id }) => id),
 | 
					          albumUsers,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      updateAlbumInfo(album);
 | 
					      updateAlbumInfo(album);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,12 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<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 { mdiDotsVertical } from '@mdi/js';
 | 
				
			||||||
  import { createEventDispatcher, onMount } from 'svelte';
 | 
					  import { createEventDispatcher, onMount } from 'svelte';
 | 
				
			||||||
  import { getContextMenuPosition } from '../../utils/context-menu';
 | 
					  import { getContextMenuPosition } from '../../utils/context-menu';
 | 
				
			||||||
@ -17,6 +24,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const dispatch = createEventDispatcher<{
 | 
					  const dispatch = createEventDispatcher<{
 | 
				
			||||||
    remove: string;
 | 
					    remove: string;
 | 
				
			||||||
 | 
					    refreshAlbum: void;
 | 
				
			||||||
  }>();
 | 
					  }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let currentUser: UserResponseDto;
 | 
					  let currentUser: UserResponseDto;
 | 
				
			||||||
@ -63,6 +71,19 @@
 | 
				
			|||||||
      selectedRemoveUser = null;
 | 
					      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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if !selectedRemoveUser}
 | 
					{#if !selectedRemoveUser}
 | 
				
			||||||
@ -78,7 +99,7 @@
 | 
				
			|||||||
          <p class="text-sm">Owner</p>
 | 
					          <p class="text-sm">Owner</p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      {#each album.sharedUsers as user}
 | 
					      {#each album.albumUsers as { user, role }}
 | 
				
			||||||
        <div
 | 
					        <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"
 | 
					          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>
 | 
					            <p class="text-sm font-medium">{user.name}</p>
 | 
				
			||||||
          </div>
 | 
					          </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}
 | 
					            {#if isOwned}
 | 
				
			||||||
              <div>
 | 
					              <div>
 | 
				
			||||||
                <CircleIconButton
 | 
					                <CircleIconButton
 | 
				
			||||||
@ -101,6 +129,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                {#if selectedMenuUser === user}
 | 
					                {#if selectedMenuUser === user}
 | 
				
			||||||
                  <ContextMenu {...position} on:outclick={() => (selectedMenuUser = null)}>
 | 
					                  <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" />
 | 
					                    <MenuOption on:click={handleMenuRemove} text="Remove" />
 | 
				
			||||||
                  </ContextMenu>
 | 
					                  </ContextMenu>
 | 
				
			||||||
                {/if}
 | 
					                {/if}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +1,36 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					  import Dropdown from '$lib/components/elements/dropdown.svelte';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.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 { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
 | 
					    AlbumUserRole,
 | 
				
			||||||
    getAllSharedLinks,
 | 
					    getAllSharedLinks,
 | 
				
			||||||
    getAllUsers,
 | 
					    getAllUsers,
 | 
				
			||||||
    type AlbumResponseDto,
 | 
					    type AlbumResponseDto,
 | 
				
			||||||
 | 
					    type AlbumUserAddDto,
 | 
				
			||||||
    type SharedLinkResponseDto,
 | 
					    type SharedLinkResponseDto,
 | 
				
			||||||
    type UserResponseDto,
 | 
					    type UserResponseDto,
 | 
				
			||||||
  } from '@immich/sdk';
 | 
					  } 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 { createEventDispatcher, onMount } from 'svelte';
 | 
				
			||||||
  import Button from '../elements/buttons/button.svelte';
 | 
					  import Button from '../elements/buttons/button.svelte';
 | 
				
			||||||
  import UserAvatar from '../shared-components/user-avatar.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 album: AlbumResponseDto;
 | 
				
			||||||
  export let onClose: () => void;
 | 
					  export let onClose: () => void;
 | 
				
			||||||
  let users: UserResponseDto[] = [];
 | 
					  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<{
 | 
					  const dispatch = createEventDispatcher<{
 | 
				
			||||||
    select: UserResponseDto[];
 | 
					    select: AlbumUserAddDto[];
 | 
				
			||||||
    share: void;
 | 
					    share: void;
 | 
				
			||||||
  }>();
 | 
					  }>();
 | 
				
			||||||
  let sharedLinks: SharedLinkResponseDto[] = [];
 | 
					  let sharedLinks: SharedLinkResponseDto[] = [];
 | 
				
			||||||
@ -43,57 +52,79 @@
 | 
				
			|||||||
    sharedLinks = data.filter((link) => link.album?.id === album.id);
 | 
					    sharedLinks = data.filter((link) => link.album?.id === album.id);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelect = (user: UserResponseDto) => {
 | 
					  const handleToggle = (user: UserResponseDto) => {
 | 
				
			||||||
    selectedUsers = selectedUsers.includes(user)
 | 
					    if (Object.keys(selectedUsers).includes(user.id)) {
 | 
				
			||||||
      ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
 | 
					      delete selectedUsers[user.id];
 | 
				
			||||||
      : [...selectedUsers, user];
 | 
					      selectedUsers = selectedUsers;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleUnselect = (user: UserResponseDto) => {
 | 
					  const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
 | 
				
			||||||
    selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
 | 
					    if (role === 'none') {
 | 
				
			||||||
 | 
					      delete selectedUsers[user.id];
 | 
				
			||||||
 | 
					      selectedUsers = selectedUsers;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectedUsers[user.id].role = role;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<FullScreenModal id="user-selection-modal" title="Invite to album" showLogo {onClose}>
 | 
					<FullScreenModal id="user-selection-modal" title="Invite to album" showLogo {onClose}>
 | 
				
			||||||
  {#if selectedUsers.length > 0}
 | 
					  {#if Object.keys(selectedUsers).length > 0}
 | 
				
			||||||
    <div class="mb-2 flex flex-wrap place-items-center gap-4 overflow-x-auto px-5 py-2 sticky">
 | 
					    <div class="mb-2 py-2 sticky">
 | 
				
			||||||
      <p class="font-medium">To</p>
 | 
					      <p class="text-xs font-medium">SELECTED</p>
 | 
				
			||||||
 | 
					      <div class="my-2">
 | 
				
			||||||
      {#each selectedUsers as user}
 | 
					        {#each Object.values(selectedUsers) as { user }}
 | 
				
			||||||
          {#key user.id}
 | 
					          {#key user.id}
 | 
				
			||||||
          <button
 | 
					            <div class="flex place-items-center gap-4 p-4">
 | 
				
			||||||
            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)}
 | 
					 | 
				
			||||||
              <div
 | 
					              <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} />
 | 
					                <Icon path={mdiCheck} size={24} />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            {:else}
 | 
					
 | 
				
			||||||
              <UserAvatar {user} size="md" />
 | 
					              <!-- <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>
 | 
				
			||||||
 | 
					          {/key}
 | 
				
			||||||
 | 
					        {/each}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="text-left">
 | 
					  {#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">
 | 
					                  <p class="text-immich-fg dark:text-immich-dark-fg">
 | 
				
			||||||
                    {user.name}
 | 
					                    {user.name}
 | 
				
			||||||
                  </p>
 | 
					                  </p>
 | 
				
			||||||
@ -102,12 +133,10 @@
 | 
				
			|||||||
                  </p>
 | 
					                  </p>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/if}
 | 
				
			||||||
        {/each}
 | 
					        {/each}
 | 
				
			||||||
      </div>
 | 
					      </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}
 | 
					    {/if}
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -117,8 +146,12 @@
 | 
				
			|||||||
        size="sm"
 | 
					        size="sm"
 | 
				
			||||||
        fullwidth
 | 
					        fullwidth
 | 
				
			||||||
        rounded="full"
 | 
					        rounded="full"
 | 
				
			||||||
        disabled={selectedUsers.length === 0}
 | 
					        disabled={Object.keys(selectedUsers).length === 0}
 | 
				
			||||||
        on:click={() => dispatch('select', selectedUsers)}>Add</Button
 | 
					        on:click={() =>
 | 
				
			||||||
 | 
					          dispatch(
 | 
				
			||||||
 | 
					            'select',
 | 
				
			||||||
 | 
					            Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
 | 
				
			||||||
 | 
					          )}>Add</Button
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { afterNavigate, goto } from '$app/navigation';
 | 
					  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 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 ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
 | 
				
			||||||
  import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
 | 
					  import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
 | 
				
			||||||
  import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
 | 
					  import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
 | 
				
			||||||
@ -39,12 +42,16 @@
 | 
				
			|||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
 | 
					  import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
 | 
				
			||||||
  import { user } from '$lib/stores/user.store';
 | 
					  import { user } from '$lib/stores/user.store';
 | 
				
			||||||
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { downloadAlbum } from '$lib/utils/asset-utils';
 | 
					  import { downloadAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { clickOutside } from '$lib/utils/click-outside';
 | 
					  import { clickOutside } from '$lib/utils/click-outside';
 | 
				
			||||||
  import { getContextMenuPosition } from '$lib/utils/context-menu';
 | 
					  import { getContextMenuPosition } from '$lib/utils/context-menu';
 | 
				
			||||||
  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
 | 
					    AlbumUserRole,
 | 
				
			||||||
 | 
					    AssetOrder,
 | 
				
			||||||
    ReactionLevel,
 | 
					    ReactionLevel,
 | 
				
			||||||
    ReactionType,
 | 
					    ReactionType,
 | 
				
			||||||
    addAssetsToAlbum,
 | 
					    addAssetsToAlbum,
 | 
				
			||||||
@ -57,29 +64,23 @@
 | 
				
			|||||||
    getAlbumInfo,
 | 
					    getAlbumInfo,
 | 
				
			||||||
    updateAlbumInfo,
 | 
					    updateAlbumInfo,
 | 
				
			||||||
    type ActivityResponseDto,
 | 
					    type ActivityResponseDto,
 | 
				
			||||||
    type UserResponseDto,
 | 
					    type AlbumUserAddDto,
 | 
				
			||||||
    AssetOrder,
 | 
					 | 
				
			||||||
  } from '@immich/sdk';
 | 
					  } from '@immich/sdk';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
    mdiArrowLeft,
 | 
					    mdiArrowLeft,
 | 
				
			||||||
 | 
					    mdiCogOutline,
 | 
				
			||||||
    mdiDeleteOutline,
 | 
					    mdiDeleteOutline,
 | 
				
			||||||
    mdiDotsVertical,
 | 
					    mdiDotsVertical,
 | 
				
			||||||
    mdiFolderDownloadOutline,
 | 
					    mdiFolderDownloadOutline,
 | 
				
			||||||
    mdiLink,
 | 
					 | 
				
			||||||
    mdiPlus,
 | 
					 | 
				
			||||||
    mdiShareVariantOutline,
 | 
					 | 
				
			||||||
    mdiPresentationPlay,
 | 
					 | 
				
			||||||
    mdiCogOutline,
 | 
					 | 
				
			||||||
    mdiImageOutline,
 | 
					    mdiImageOutline,
 | 
				
			||||||
    mdiImagePlusOutline,
 | 
					    mdiImagePlusOutline,
 | 
				
			||||||
 | 
					    mdiLink,
 | 
				
			||||||
 | 
					    mdiPlus,
 | 
				
			||||||
 | 
					    mdiPresentationPlay,
 | 
				
			||||||
 | 
					    mdiShareVariantOutline,
 | 
				
			||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
  import { fly } from 'svelte/transition';
 | 
					  import { fly } from 'svelte/transition';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  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;
 | 
					  export let data: PageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -137,6 +138,9 @@
 | 
				
			|||||||
  $: showActivityStatus =
 | 
					  $: showActivityStatus =
 | 
				
			||||||
    album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
 | 
					    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 }) => {
 | 
					  afterNavigate(({ from }) => {
 | 
				
			||||||
    let url: string | undefined = from?.url?.pathname;
 | 
					    let url: string | undefined = from?.url?.pathname;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -312,12 +316,12 @@
 | 
				
			|||||||
    viewMode = ViewMode.VIEW;
 | 
					    viewMode = ViewMode.VIEW;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddUsers = async (users: UserResponseDto[]) => {
 | 
					  const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      album = await addUsersToAlbum({
 | 
					      album = await addUsersToAlbum({
 | 
				
			||||||
        id: album.id,
 | 
					        id: album.id,
 | 
				
			||||||
        addUsersDto: {
 | 
					        addUsersDto: {
 | 
				
			||||||
          sharedUserIds: [...users].map(({ id }) => id),
 | 
					          albumUsers,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -335,9 +339,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await refreshAlbum();
 | 
					      await refreshAlbum();
 | 
				
			||||||
      viewMode = album.sharedUsers.length > 1 ? ViewMode.SELECT_USERS : ViewMode.VIEW;
 | 
					      viewMode = album.sharedUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW;
 | 
				
			||||||
    } catch (error) {
 | 
					    } 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}
 | 
					      {#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS}
 | 
				
			||||||
        <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
 | 
					        <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
 | 
				
			||||||
          <svelte:fragment slot="trailing">
 | 
					          <svelte:fragment slot="trailing">
 | 
				
			||||||
 | 
					            {#if isEditor}
 | 
				
			||||||
              <CircleIconButton
 | 
					              <CircleIconButton
 | 
				
			||||||
                title="Add photos"
 | 
					                title="Add photos"
 | 
				
			||||||
                on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
 | 
					                on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
 | 
				
			||||||
                icon={mdiImagePlusOutline}
 | 
					                icon={mdiImagePlusOutline}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
 | 
					            {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {#if isOwned}
 | 
					            {#if isOwned}
 | 
				
			||||||
              <CircleIconButton
 | 
					              <CircleIconButton
 | 
				
			||||||
@ -578,13 +584,25 @@
 | 
				
			|||||||
                      <UserAvatar user={album.owner} size="md" />
 | 
					                      <UserAvatar user={album.owner} size="md" />
 | 
				
			||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <!-- users -->
 | 
					                    <!-- users with write access (collaborators) -->
 | 
				
			||||||
                    {#each album.sharedUsers as user (user.id)}
 | 
					                    {#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
 | 
				
			||||||
                      <button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
 | 
					                      <button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
 | 
				
			||||||
                        <UserAvatar {user} size="md" />
 | 
					                        <UserAvatar {user} size="md" />
 | 
				
			||||||
                      </button>
 | 
					                      </button>
 | 
				
			||||||
                    {/each}
 | 
					                    {/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}
 | 
					                    {#if isOwned}
 | 
				
			||||||
                      <CircleIconButton
 | 
					                      <CircleIconButton
 | 
				
			||||||
                        backgroundColor="#d3d3d3"
 | 
					                        backgroundColor="#d3d3d3"
 | 
				
			||||||
@ -678,6 +696,7 @@
 | 
				
			|||||||
    onClose={() => (viewMode = ViewMode.VIEW)}
 | 
					    onClose={() => (viewMode = ViewMode.VIEW)}
 | 
				
			||||||
    {album}
 | 
					    {album}
 | 
				
			||||||
    on:remove={({ detail: userId }) => handleRemoveUser(userId)}
 | 
					    on:remove={({ detail: userId }) => handleRemoveUser(userId)}
 | 
				
			||||||
 | 
					    on:refreshAlbum={refreshAlbum}
 | 
				
			||||||
  />
 | 
					  />
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
 | 
				
			|||||||
  owner: userFactory.build(),
 | 
					  owner: userFactory.build(),
 | 
				
			||||||
  shared: false,
 | 
					  shared: false,
 | 
				
			||||||
  sharedUsers: [],
 | 
					  sharedUsers: [],
 | 
				
			||||||
 | 
					  albumUsers: [],
 | 
				
			||||||
  hasSharedLink: false,
 | 
					  hasSharedLink: false,
 | 
				
			||||||
  isActivityEnabled: true,
 | 
					  isActivityEnabled: true,
 | 
				
			||||||
  order: AssetOrder.Desc,
 | 
					  order: AssetOrder.Desc,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user