fix(server): live photo relation (#10637)
* fix(server): live photo relation * handle deletion and unit test * lint * chore: clean up and e2e tests * fix test * sql --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -124,7 +124,7 @@ export class AssetEntity {
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: boolean;
|
||||
|
||||
@OneToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||
@ManyToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||
@JoinColumn()
|
||||
livePhotoVideo!: AssetEntity | null;
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ export interface IAssetRepository {
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||
getLivePhotoCount(motionId: string): Promise<number>;
|
||||
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
|
||||
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
||||
update(asset: AssetUpdateOptions): Promise<void>;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class FixLivePhotoVideoRelation1719359859887 implements MigrationInterface {
|
||||
name = 'FixLivePhotoVideoRelation1719359859887'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_16294b83fa8c0149719a1f631ef"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_16294b83fa8c0149719a1f631ef"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef" UNIQUE ("livePhotoVideoId")`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -377,6 +377,14 @@ WHERE
|
||||
AND ("AssetEntity"."isVisible" = $3)
|
||||
)
|
||||
|
||||
-- AssetRepository.getLivePhotoCount
|
||||
SELECT
|
||||
COUNT(1) AS "cnt"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
(("AssetEntity"."livePhotoVideoId" = $1))
|
||||
|
||||
-- AssetRepository.getById
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id",
|
||||
|
||||
@@ -249,6 +249,16 @@ export class AssetRepository implements IAssetRepository {
|
||||
return items.map((asset) => asset.deviceAssetId);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getLivePhotoCount(motionId: string): Promise<number> {
|
||||
return this.repository.count({
|
||||
where: {
|
||||
livePhotoVideoId: motionId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(
|
||||
id: string,
|
||||
|
||||
@@ -171,6 +171,7 @@ export class AssetMediaService {
|
||||
}
|
||||
if (motionAsset.isVisible) {
|
||||
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, auth.user.id, motionAsset.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,6 +445,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
assetMock.getLivePhotoCount.mockResolvedValue(0);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
@@ -472,6 +473,27 @@ describe(AssetService.name, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||
assetMock.getLivePhotoCount.mockResolvedValue(2);
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
deleteOnDisk: true,
|
||||
});
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.DELETE_FILES,
|
||||
data: {
|
||||
files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'],
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update usage', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
||||
|
||||
@@ -304,12 +304,15 @@ export class AssetService {
|
||||
}
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id);
|
||||
|
||||
// TODO refactor this to use cascades
|
||||
// delete the motion if it is not used by another asset
|
||||
if (asset.livePhotoVideoId) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.ASSET_DELETION,
|
||||
data: { id: asset.livePhotoVideoId, deleteOnDisk },
|
||||
});
|
||||
const count = await this.assetRepository.getLivePhotoCount(asset.livePhotoVideoId);
|
||||
if (count === 0) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.ASSET_DELETION,
|
||||
data: { id: asset.livePhotoVideoId, deleteOnDisk },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
|
||||
|
||||
Reference in New Issue
Block a user