feat(server): visibility column (#17939)

* feat: private view

* pr feedback

* sql generation

* feat: visibility column

* fix: set visibility value as the same as the still part after unlinked live photos

* fix: test

* pr feedback
This commit is contained in:
Alex
2025-05-06 12:12:48 -05:00
committed by GitHub
parent 016d7a6ceb
commit d33ce13561
90 changed files with 1137 additions and 867 deletions

View File

@@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
import { AssetStats } from 'src/repositories/asset.repository';
import { AssetService } from 'src/services/asset.service';
import { assetStub } from 'test/fixtures/asset.stub';
@@ -46,14 +46,22 @@ describe(AssetService.name, () => {
describe('getStatistics', () => {
it('should get the statistics for a user, excluding archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual(
statResponse,
);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.TIMELINE,
});
});
it('should get the statistics for a user for archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual(
statResponse,
);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.ARCHIVE,
});
});
it('should get the statistics for a user for favorite assets', async () => {
@@ -192,9 +200,9 @@ describe(AssetService.name, () => {
describe('update', () => {
it('should require asset write access for the id', async () => {
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
BadRequestException,
);
await expect(
sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.asset.update).not.toHaveBeenCalled();
});
@@ -242,7 +250,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@@ -263,7 +274,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@@ -284,7 +298,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@@ -296,7 +313,7 @@ describe(AssetService.name, () => {
mocks.asset.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset,
ownerId: authStub.admin.user.id,
isVisible: true,
visibility: AssetVisibility.TIMELINE,
});
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
mocks.asset.update.mockResolvedValue(assetStub.image);
@@ -305,7 +322,10 @@ describe(AssetService.name, () => {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@@ -335,7 +355,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: null,
});
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: assetStub.livePhotoStillAsset.visibility,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@@ -361,7 +384,6 @@ describe(AssetService.name, () => {
await expect(
sut.updateAll(authStub.admin, {
ids: ['asset-1'],
isArchived: false,
}),
).rejects.toBeInstanceOf(BadRequestException);
});
@@ -369,9 +391,11 @@ describe(AssetService.name, () => {
it('should update all assets', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE });
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
visibility: AssetVisibility.ARCHIVE,
});
});
it('should not update Assets table if no relevant fields are provided', async () => {
@@ -381,7 +405,6 @@ describe(AssetService.name, () => {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
isFavorite: undefined,
duplicateId: undefined,
rating: undefined,
@@ -389,14 +412,14 @@ describe(AssetService.name, () => {
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
});
it('should update Assets table if isArchived field is provided', async () => {
it('should update Assets table if visibility field is provided', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.updateAll(authStub.admin, {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
visibility: undefined,
isFavorite: false,
duplicateId: undefined,
rating: undefined,
@@ -416,7 +439,6 @@ describe(AssetService.name, () => {
latitude: 30,
longitude: 50,
dateTimeOriginal,
isArchived: undefined,
isFavorite: false,
duplicateId: undefined,
rating: undefined,
@@ -439,7 +461,6 @@ describe(AssetService.name, () => {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
isFavorite: undefined,
duplicateId: null,
rating: undefined,