diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 0215795188..d2609653e7 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -1,5 +1,14 @@ -- NOTE: This file is auto generated by ./sql-generator +-- AssetRepository.upsertExif +insert into + "asset_exif" ("dateTimeOriginal", "lockedProperties") +values + ($1, $2) +on conflict ("assetId") do update +set + "dateTimeOriginal" = "excluded"."dateTimeOriginal" + -- AssetRepository.updateAllExif update "asset_exif" set diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 069a5f4b19..149143255d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -7,7 +7,7 @@ import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; -import { AssetExifTable, LockableProperty } from 'src/schema/tables/asset-exif.table'; +import { AssetExifTable, lockableProperties, LockableProperty } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; @@ -120,13 +120,26 @@ const distinctLocked = (eb: ExpressionBuild export class AssetRepository { constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [{ dateTimeOriginal: DummyValue.DATE }, { lockedPropertiesBehavior: 'update' }] }) async upsertExif( exif: Insertable, { lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' }, ): Promise { + const values = { + ...exif, + }; + + if (lockedPropertiesBehavior !== 'none') { + delete values.lockedProperties; + } + + if (lockedPropertiesBehavior === 'update') { + values.lockedProperties = lockableProperties.filter((property) => Object.keys(exif).includes(property)); + } + await this.db .insertInto('asset_exif') - .values(exif) + .values(values) .onConflict((oc) => oc.column('assetId').doUpdateSet((eb) => { const updateLocked = (col: T) => eb.ref(`excluded.${col}`); @@ -138,43 +151,45 @@ export class AssetRepository { .else(eb.ref(`excluded.${col}`)) .end(); const ref = lockedPropertiesBehavior === 'update' ? updateLocked : skipLocked; - return removeUndefinedKeys( - { - description: ref('description'), - exifImageWidth: ref('exifImageWidth'), - exifImageHeight: ref('exifImageHeight'), - fileSizeInByte: ref('fileSizeInByte'), - orientation: ref('orientation'), - dateTimeOriginal: ref('dateTimeOriginal'), - modifyDate: ref('modifyDate'), - timeZone: ref('timeZone'), - latitude: ref('latitude'), - longitude: ref('longitude'), - projectionType: ref('projectionType'), - city: ref('city'), - livePhotoCID: ref('livePhotoCID'), - autoStackId: ref('autoStackId'), - state: ref('state'), - country: ref('country'), - make: ref('make'), - model: ref('model'), - lensModel: ref('lensModel'), - fNumber: ref('fNumber'), - focalLength: ref('focalLength'), - iso: ref('iso'), - exposureTime: ref('exposureTime'), - profileDescription: ref('profileDescription'), - colorspace: ref('colorspace'), - bitsPerSample: ref('bitsPerSample'), - rating: ref('rating'), - fps: ref('fps'), - lockedProperties: - exif.lockedProperties !== undefined && lockedPropertiesBehavior !== 'none' - ? distinctLocked(eb, exif.lockedProperties) - : exif.lockedProperties, - }, - exif, - ); + return { + ...removeUndefinedKeys( + { + description: ref('description'), + exifImageWidth: ref('exifImageWidth'), + exifImageHeight: ref('exifImageHeight'), + fileSizeInByte: ref('fileSizeInByte'), + orientation: ref('orientation'), + dateTimeOriginal: ref('dateTimeOriginal'), + modifyDate: ref('modifyDate'), + timeZone: ref('timeZone'), + latitude: ref('latitude'), + longitude: ref('longitude'), + projectionType: ref('projectionType'), + city: ref('city'), + livePhotoCID: ref('livePhotoCID'), + autoStackId: ref('autoStackId'), + state: ref('state'), + country: ref('country'), + make: ref('make'), + model: ref('model'), + lensModel: ref('lensModel'), + fNumber: ref('fNumber'), + focalLength: ref('focalLength'), + iso: ref('iso'), + exposureTime: ref('exposureTime'), + profileDescription: ref('profileDescription'), + colorspace: ref('colorspace'), + bitsPerSample: ref('bitsPerSample'), + rating: ref('rating'), + fps: ref('fps'), + lockedProperties: + lockedPropertiesBehavior === 'update' + ? distinctLocked(eb, values.lockedProperties ?? []) + : ref('lockedProperties'), + }, + values, + ), + }; }), ) .execute(); diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 13bc1ca9a9..8b54019fcf 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -268,4 +268,65 @@ describe(AssetService.name, () => { }); }); }); + + describe('update', () => { + it('should update dateTimeOriginal', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00' }), + }), + ); + }); + }); + + describe('updateAll', () => { + it('should relatively update assets', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11 }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-19T18:00:00+00:00', + }), + }), + ); + }); + }); });