From 5e3f5f2b55c44a128db04a3a4d8952a9fad3b764 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 12 Jan 2026 07:01:38 -0600 Subject: [PATCH] fix: unlock properties after successful sidecar write (#25168) --- server/src/queries/asset.repository.sql | 17 ++++++ server/src/repositories/asset.repository.ts | 11 ++++ server/src/services/metadata.service.spec.ts | 6 ++ server/src/services/metadata.service.ts | 2 + .../repositories/asset.repository.spec.ts | 60 +++++++++++++++++++ .../repositories/asset.repository.mock.ts | 1 + 6 files changed, 97 insertions(+) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index aaa7dd46fb..666f41eb09 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -49,6 +49,23 @@ returning "dateTimeOriginal", "timeZone" +-- AssetRepository.unlockProperties +update "asset_exif" +set + "lockedProperties" = nullif( + array( + select distinct + property + from + unnest("asset_exif"."lockedProperties") property + where + not property = any ($1) + ), + '{}' + ) +where + "assetId" = $2 + -- AssetRepository.getMetadata select "key", diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 7ae6a277b7..325835b965 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -223,6 +223,17 @@ export class AssetRepository { .execute(); } + @GenerateSql({ params: [DummyValue.UUID, ['description']] }) + unlockProperties(assetId: string, properties: LockableProperty[]) { + return this.db + .updateTable('asset_exif') + .where('assetId', '=', assetId) + .set((eb) => ({ + lockedProperties: sql`nullif(array(select distinct property from unnest(${eb.ref('asset_exif.lockedProperties')}) property where not property = any(${properties})), '{}')`, + })) + .execute(); + } + async upsertJobStatus(...jobStatus: Insertable[]): Promise { if (jobStatus.length === 0) { return; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index e6d6a523b1..b10325998e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1758,6 +1758,12 @@ describe(MetadataService.name, () => { GPSLatitude: gps, GPSLongitude: gps, }); + expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, [ + 'description', + 'latitude', + 'longitude', + 'dateTimeOriginal', + ]); }); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index c9535f3612..e6cc15bc77 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -461,6 +461,8 @@ export class MetadataService extends BaseService { await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: sidecarPath }); } + await this.assetRepository.unlockProperties(asset.id, lockedProperties); + return JobStatus.Success; } diff --git a/server/test/medium/specs/repositories/asset.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts index a7af66f872..97f503e9ed 100644 --- a/server/test/medium/specs/repositories/asset.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -87,4 +87,64 @@ describe(AssetRepository.name, () => { ).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] }); }); }); + + describe('unlockProperties', () => { + it('should unlock one property', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal', 'description'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] }); + + await sut.unlockProperties(asset.id, ['dateTimeOriginal']); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['description'] }); + }); + + it('should unlock all properties', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal', 'description'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] }); + + await sut.unlockProperties(asset.id, ['description', 'dateTimeOriginal']); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + }); + }); }); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 4847c84a35..da57485382 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -9,6 +9,7 @@ export const newAssetRepositoryMock = (): Mocked