mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
feat: readonly album sharing (#8720)
* rename albums_shared_users_users to album_permissions and add readonly column * disable synchronize on the original join table * remove unnecessary FK names * set readonly=true as default for new album shares * separate and implement album READ and WRITE permission * expose albumPermissions on the API, deprecate sharedUsers * generate openapi * create readonly view on frontend * ??? move slideshow button out from ellipsis menu so that non-owners can have access too * correct sharedUsers joins * add album permission repository * remove a log * fix assetCount getting reset when adding users * fix lint * add set permission endpoint and UI * sort users * remove log * Revert "??? move slideshow button out from ellipsis menu so that non-owners can have access too" This reverts commit 1343bfa31125f7136f81db28f7aa4c5ef0204847. * rename stuff * fix db schema annotations * sql generate * change readonly default to follow migration * fix deprecation notice * change readonly boolean to role enum * fix joincolumn as primary key * rename albumUserRepository in album service * clean up userId and albumId * add write access to shared link * fix existing tests * switch to vitest * format and fix tests on web * add new test * fix one e2e test * rename new API field to albumUsers * capitalize serverside enum * remove unused ReadWrite type * missed rename from previous commit * rename to albumUsers in album entity as well * remove outdated Equals calls * unnecessary relation * rename to updateUser in album service * minor renamery * move sorting to backend * rename and separate ALBUM_WRITE as ADD_ASSET and REMOVE_ASSET * fix tests * fix "should migrate single moving picture" test failing on European system timezone * generated changes after merge * lint fix * fix correct page to open after removing user from album * fix e2e tests and some bugs * rename updateAlbumUser rest endpoint * add new e2e tests for updateAlbumUser endpoint * small optimizations * refactor album e2e test, add new album shared with viewer * add new test to check if viewer can see the album * add new e2e tests for readonly share * failing test: User delete doesn't cascade to UserAlbum entity * fix: handle deleted users * use lodash for sort * add role to addUsersToAlbum endpoint * add UI for adding editors * lint fixes * change role back to editor as DB default * fix server tests * redesign user selection modal editor selector * style tweaks * fix type error * Revert "style tweaks" This reverts commit ab604f4c8f3a6f12ab0b5fe2dd2ede723aa68775. * Revert "redesign user selection modal editor selector" This reverts commit e6f344856c6c05e4eb5c78f0dffb9f52498795f4. * chore: cleanup and improve add user modal * chore: open api * small styling --------- Co-authored-by: mgabor <> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
0b3373c552
commit
2943f93098
@ -1,12 +1,13 @@
|
|||||||
import {
|
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,71 +52,91 @@
|
|||||||
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" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="text-left">
|
<!-- <UserAvatar {user} size="md" /> -->
|
||||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
<div class="text-left flex-grow">
|
||||||
{user.name}
|
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||||
</p>
|
{user.name}
|
||||||
<p class="text-xs">
|
</p>
|
||||||
{user.email}
|
<p class="text-xs">
|
||||||
</p>
|
{user.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
title="Role"
|
||||||
|
options={roleOptions}
|
||||||
|
render={({ title, icon }) => ({ title, icon })}
|
||||||
|
on:select={({ detail: { value } }) => handleChangeRole(user, value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
{/key}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if users.length + Object.keys(selectedUsers).length === 0}
|
||||||
|
<p class="p-5 text-sm">
|
||||||
|
Looks like you have shared this album with all users or you don't have any user to share with.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
|
||||||
|
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
|
||||||
|
<p class="text-xs font-medium">SUGGESTIONS</p>
|
||||||
|
|
||||||
|
<div class="my-2">
|
||||||
|
{#each users as user}
|
||||||
|
{#if !Object.keys(selectedUsers).includes(user.id)}
|
||||||
|
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
|
||||||
|
<button on:click={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
|
||||||
|
<UserAvatar {user} size="md" />
|
||||||
|
<div class="text-left flex-grow">
|
||||||
|
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||||
|
{user.name}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs">
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/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">
|
||||||
<CircleIconButton
|
{#if isEditor}
|
||||||
title="Add photos"
|
<CircleIconButton
|
||||||
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
|
title="Add photos"
|
||||||
icon={mdiImagePlusOutline}
|
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
|
||||||
/>
|
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