fix: updating lockable properties

This commit is contained in:
Daniel Dietzler
2025-12-12 10:33:26 -06:00
parent 98f3883371
commit cad095a19e
3 changed files with 124 additions and 39 deletions

View File

@@ -1,5 +1,14 @@
-- NOTE: This file is auto generated by ./sql-generator -- 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 -- AssetRepository.updateAllExif
update "asset_exif" update "asset_exif"
set set

View File

@@ -7,7 +7,7 @@ import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { DB } from 'src/schema'; 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 { AssetFileTable } from 'src/schema/tables/asset-file.table';
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
import { AssetTable } from 'src/schema/tables/asset.table'; import { AssetTable } from 'src/schema/tables/asset.table';
@@ -120,13 +120,26 @@ const distinctLocked = <T extends LockableProperty[] | null>(eb: ExpressionBuild
export class AssetRepository { export class AssetRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ dateTimeOriginal: DummyValue.DATE }, { lockedPropertiesBehavior: 'update' }] })
async upsertExif( async upsertExif(
exif: Insertable<AssetExifTable>, exif: Insertable<AssetExifTable>,
{ lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' }, { lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' },
): Promise<void> { ): Promise<void> {
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 await this.db
.insertInto('asset_exif') .insertInto('asset_exif')
.values(exif) .values(values)
.onConflict((oc) => .onConflict((oc) =>
oc.column('assetId').doUpdateSet((eb) => { oc.column('assetId').doUpdateSet((eb) => {
const updateLocked = <T extends keyof AssetExifTable>(col: T) => eb.ref(`excluded.${col}`); const updateLocked = <T extends keyof AssetExifTable>(col: T) => eb.ref(`excluded.${col}`);
@@ -138,43 +151,45 @@ export class AssetRepository {
.else(eb.ref(`excluded.${col}`)) .else(eb.ref(`excluded.${col}`))
.end(); .end();
const ref = lockedPropertiesBehavior === 'update' ? updateLocked : skipLocked; const ref = lockedPropertiesBehavior === 'update' ? updateLocked : skipLocked;
return removeUndefinedKeys( return {
{ ...removeUndefinedKeys(
description: ref('description'), {
exifImageWidth: ref('exifImageWidth'), description: ref('description'),
exifImageHeight: ref('exifImageHeight'), exifImageWidth: ref('exifImageWidth'),
fileSizeInByte: ref('fileSizeInByte'), exifImageHeight: ref('exifImageHeight'),
orientation: ref('orientation'), fileSizeInByte: ref('fileSizeInByte'),
dateTimeOriginal: ref('dateTimeOriginal'), orientation: ref('orientation'),
modifyDate: ref('modifyDate'), dateTimeOriginal: ref('dateTimeOriginal'),
timeZone: ref('timeZone'), modifyDate: ref('modifyDate'),
latitude: ref('latitude'), timeZone: ref('timeZone'),
longitude: ref('longitude'), latitude: ref('latitude'),
projectionType: ref('projectionType'), longitude: ref('longitude'),
city: ref('city'), projectionType: ref('projectionType'),
livePhotoCID: ref('livePhotoCID'), city: ref('city'),
autoStackId: ref('autoStackId'), livePhotoCID: ref('livePhotoCID'),
state: ref('state'), autoStackId: ref('autoStackId'),
country: ref('country'), state: ref('state'),
make: ref('make'), country: ref('country'),
model: ref('model'), make: ref('make'),
lensModel: ref('lensModel'), model: ref('model'),
fNumber: ref('fNumber'), lensModel: ref('lensModel'),
focalLength: ref('focalLength'), fNumber: ref('fNumber'),
iso: ref('iso'), focalLength: ref('focalLength'),
exposureTime: ref('exposureTime'), iso: ref('iso'),
profileDescription: ref('profileDescription'), exposureTime: ref('exposureTime'),
colorspace: ref('colorspace'), profileDescription: ref('profileDescription'),
bitsPerSample: ref('bitsPerSample'), colorspace: ref('colorspace'),
rating: ref('rating'), bitsPerSample: ref('bitsPerSample'),
fps: ref('fps'), rating: ref('rating'),
lockedProperties: fps: ref('fps'),
exif.lockedProperties !== undefined && lockedPropertiesBehavior !== 'none' lockedProperties:
? distinctLocked(eb, exif.lockedProperties) lockedPropertiesBehavior === 'update'
: exif.lockedProperties, ? distinctLocked(eb, values.lockedProperties ?? [])
}, : ref('lockedProperties'),
exif, },
); values,
),
};
}), }),
) )
.execute(); .execute();

View File

@@ -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',
}),
}),
);
});
});
}); });