fix(server): dissolve stack when its non-primary assets are deleted (#29354)

This commit is contained in:
Aditya Raj Singh
2026-07-22 13:00:52 +02:00
committed by GitHub
parent 20997823d8
commit a0c7b81142
2 changed files with 44 additions and 9 deletions
+28
View File
@@ -568,6 +568,34 @@ describe(AssetService.name, () => {
expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId);
});
it('should delete the stack when a non-primary asset is deleted and only the primary would remain', async () => {
const asset = AssetFactory.from().build();
const deletionAsset = {
...getForAssetDeletion(asset),
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }] },
};
mocks.stack.delete.mockResolvedValue();
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
expect(mocks.stack.delete).toHaveBeenCalledWith(deletionAsset.stack.id);
});
it('should keep the stack when a non-primary asset is deleted and the primary plus another asset remain', async () => {
const asset = AssetFactory.from().build();
const deletionAsset = {
...getForAssetDeletion(asset),
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }, { id: newUuid() }] },
};
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
expect(mocks.stack.delete).not.toHaveBeenCalled();
expect(mocks.stack.update).not.toHaveBeenCalled();
});
it('should delete a live photo', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
+16 -9
View File
@@ -316,18 +316,25 @@ export class AssetService extends BaseService {
return JobStatus.Failed;
}
// replace the parent of the stack children with a new asset
if (asset.stack?.primaryAssetId === id) {
// this only includes timeline visible assets and excludes the primary asset
const stackAssetIds = asset.stack.assets.map((a) => a.id);
if (stackAssetIds.length >= 2) {
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
if (asset.stack) {
// asset.stack.assets only includes timeline visible assets and excludes the primary asset
const remainingStackAssetIds = asset.stack.assets.map((a) => a.id).filter((assetId) => assetId !== id);
// the primary survives unless it is the asset being deleted
let remainingCount = remainingStackAssetIds.length;
if (asset.stack.primaryAssetId !== id) {
remainingCount++;
}
if (remainingCount < 2) {
// 0 or 1 asset would remain: dissolve the stack so it does not linger as a single-asset stack
await this.stackRepository.delete(asset.stack.id);
} else if (asset.stack.primaryAssetId === id) {
// the primary is being deleted but others remain: promote a new primary
await this.stackRepository.update(asset.stack.id, {
id: asset.stack.id,
primaryAssetId: newPrimaryAssetId,
primaryAssetId: remainingStackAssetIds[0],
});
} else {
await this.stackRepository.delete(asset.stack.id);
}
}