mirror of
https://github.com/immich-app/immich.git
synced 2025-12-18 01:11:07 +03:00
single statement
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely';
|
import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely';
|
||||||
import { intersection, isEmpty, isUndefined, omit, omitBy, union } from 'lodash';
|
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { Stack } from 'src/database';
|
import { Stack } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
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, lockableProperties, LockableProperty } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable, 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';
|
||||||
@@ -113,6 +113,9 @@ interface GetByIdsRelations {
|
|||||||
tags?: boolean;
|
tags?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const distinctLocked = <T extends LockableProperty[] | null>(eb: ExpressionBuilder<DB, 'asset_exif'>, columns: T) =>
|
||||||
|
sql<T>`nullif(array(select distinct unnest(${eb.ref('asset_exif.lockedProperties')} || ${columns})), '{}')`;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetRepository {
|
export class AssetRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
@@ -121,79 +124,60 @@ export class AssetRepository {
|
|||||||
exif: Insertable<AssetExifTable>,
|
exif: Insertable<AssetExifTable>,
|
||||||
{ lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' },
|
{ lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.db.transaction().execute(async (tx) => {
|
await this.db
|
||||||
const lockedProperties = await tx
|
|
||||||
.selectFrom('asset_exif')
|
|
||||||
.select('asset_exif.lockedProperties')
|
|
||||||
.where('asset_exif.assetId', '=', exif.assetId)
|
|
||||||
.executeTakeFirst()
|
|
||||||
.then((result) => result?.lockedProperties ?? []);
|
|
||||||
|
|
||||||
let value = { ...exif, assetId: asUuid(exif.assetId) };
|
|
||||||
|
|
||||||
switch (lockedPropertiesBehavior) {
|
|
||||||
case 'skip': {
|
|
||||||
value = omit(value, [...lockedProperties, 'lockedProperties']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'update': {
|
|
||||||
const updatedLockableProperties = intersection(lockableProperties, Object.keys(exif)) as LockableProperty[];
|
|
||||||
value = {
|
|
||||||
...value,
|
|
||||||
lockedProperties: union(updatedLockableProperties, lockedProperties),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(value).length <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx
|
|
||||||
.insertInto('asset_exif')
|
.insertInto('asset_exif')
|
||||||
.values(value)
|
.values(exif)
|
||||||
.onConflict((oc) =>
|
.onConflict((oc) =>
|
||||||
oc.column('assetId').doUpdateSet((eb) =>
|
oc.column('assetId').doUpdateSet((eb) => {
|
||||||
removeUndefinedKeys(
|
const updateLocked = <T extends keyof AssetExifTable>(col: T) => eb.ref(`excluded.${col}`);
|
||||||
|
const skipLocked = <T extends keyof AssetExifTable>(col: T) =>
|
||||||
|
eb
|
||||||
|
.case()
|
||||||
|
.when(sql`${col}`, '=', eb.fn.any('asset_exif.lockedProperties'))
|
||||||
|
.then(eb.ref(`asset_exif.${col}`))
|
||||||
|
.else(eb.ref(`excluded.${col}`))
|
||||||
|
.end();
|
||||||
|
const ref = lockedPropertiesBehavior === 'update' ? updateLocked : skipLocked;
|
||||||
|
return removeUndefinedKeys(
|
||||||
{
|
{
|
||||||
description: eb.ref('excluded.description'),
|
description: ref('description'),
|
||||||
exifImageWidth: eb.ref('excluded.exifImageWidth'),
|
exifImageWidth: ref('exifImageWidth'),
|
||||||
exifImageHeight: eb.ref('excluded.exifImageHeight'),
|
exifImageHeight: ref('exifImageHeight'),
|
||||||
fileSizeInByte: eb.ref('excluded.fileSizeInByte'),
|
fileSizeInByte: ref('fileSizeInByte'),
|
||||||
orientation: eb.ref('excluded.orientation'),
|
orientation: ref('orientation'),
|
||||||
dateTimeOriginal: eb.ref('excluded.dateTimeOriginal'),
|
dateTimeOriginal: ref('dateTimeOriginal'),
|
||||||
modifyDate: eb.ref('excluded.modifyDate'),
|
modifyDate: ref('modifyDate'),
|
||||||
timeZone: eb.ref('excluded.timeZone'),
|
timeZone: ref('timeZone'),
|
||||||
latitude: eb.ref('excluded.latitude'),
|
latitude: ref('latitude'),
|
||||||
longitude: eb.ref('excluded.longitude'),
|
longitude: ref('longitude'),
|
||||||
projectionType: eb.ref('excluded.projectionType'),
|
projectionType: ref('projectionType'),
|
||||||
city: eb.ref('excluded.city'),
|
city: ref('city'),
|
||||||
livePhotoCID: eb.ref('excluded.livePhotoCID'),
|
livePhotoCID: ref('livePhotoCID'),
|
||||||
autoStackId: eb.ref('excluded.autoStackId'),
|
autoStackId: ref('autoStackId'),
|
||||||
state: eb.ref('excluded.state'),
|
state: ref('state'),
|
||||||
country: eb.ref('excluded.country'),
|
country: ref('country'),
|
||||||
make: eb.ref('excluded.make'),
|
make: ref('make'),
|
||||||
model: eb.ref('excluded.model'),
|
model: ref('model'),
|
||||||
lensModel: eb.ref('excluded.lensModel'),
|
lensModel: ref('lensModel'),
|
||||||
fNumber: eb.ref('excluded.fNumber'),
|
fNumber: ref('fNumber'),
|
||||||
focalLength: eb.ref('excluded.focalLength'),
|
focalLength: eb.ref('excluded.focalLength'),
|
||||||
iso: eb.ref('excluded.iso'),
|
iso: ref('iso'),
|
||||||
exposureTime: eb.ref('excluded.exposureTime'),
|
exposureTime: ref('exposureTime'),
|
||||||
profileDescription: eb.ref('excluded.profileDescription'),
|
profileDescription: ref('profileDescription'),
|
||||||
colorspace: eb.ref('excluded.colorspace'),
|
colorspace: ref('colorspace'),
|
||||||
bitsPerSample: eb.ref('excluded.bitsPerSample'),
|
bitsPerSample: ref('bitsPerSample'),
|
||||||
rating: eb.ref('excluded.rating'),
|
rating: ref('rating'),
|
||||||
fps: eb.ref('excluded.fps'),
|
fps: ref('fps'),
|
||||||
lockedProperties: eb.ref('excluded.lockedProperties'),
|
lockedProperties:
|
||||||
|
exif.lockedProperties === undefined || lockedPropertiesBehavior === 'none'
|
||||||
|
? undefined
|
||||||
|
: distinctLocked(eb, exif.lockedProperties),
|
||||||
},
|
},
|
||||||
value,
|
exif,
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID], { model: DummyValue.STRING }] })
|
@GenerateSql({ params: [[DummyValue.UUID], { model: DummyValue.STRING }] })
|
||||||
@@ -207,11 +191,7 @@ export class AssetRepository {
|
|||||||
.updateTable('asset_exif')
|
.updateTable('asset_exif')
|
||||||
.set((eb) => ({
|
.set((eb) => ({
|
||||||
...options,
|
...options,
|
||||||
lockedProperties: eb
|
lockedProperties: distinctLocked(eb, Object.keys(options) as LockableProperty[]),
|
||||||
.fn<
|
|
||||||
LockableProperty[]
|
|
||||||
>('array', [sql`select distinct unnest(${eb.fn('array_cat', ['lockedProperties', eb.val(Object.keys(options))])})`])
|
|
||||||
.as('lockedProperties').expression,
|
|
||||||
}))
|
}))
|
||||||
.where('assetId', 'in', ids)
|
.where('assetId', 'in', ids)
|
||||||
.execute();
|
.execute();
|
||||||
@@ -219,21 +199,17 @@ export class AssetRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.NUMBER, DummyValue.STRING] })
|
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.NUMBER, DummyValue.STRING] })
|
||||||
@Chunked()
|
@Chunked()
|
||||||
async updateDateTimeOriginal(
|
updateDateTimeOriginal(ids: string[], delta?: number, timeZone?: string) {
|
||||||
ids: string[],
|
if (ids.length === 0) {
|
||||||
delta?: number,
|
return;
|
||||||
timeZone?: string,
|
}
|
||||||
): Promise<{ assetId: string; dateTimeOriginal: Date | null; timeZone: string | null }[]> {
|
|
||||||
return await this.db
|
return this.db
|
||||||
.updateTable('asset_exif')
|
.updateTable('asset_exif')
|
||||||
.set((eb) => ({
|
.set((eb) => ({
|
||||||
dateTimeOriginal: sql`"dateTimeOriginal" + ${(delta ?? 0) + ' minute'}::interval`,
|
dateTimeOriginal: sql`"dateTimeOriginal" + ${(delta ?? 0) + ' minute'}::interval`,
|
||||||
timeZone,
|
timeZone,
|
||||||
lockedProperties: eb
|
lockedProperties: distinctLocked(eb, ['dateTimeOriginal', 'timeZone']),
|
||||||
.fn<
|
|
||||||
LockableProperty[]
|
|
||||||
>('array', [sql`select distinct unnest(${eb.fn('array_cat', ['lockedProperties', eb.val(['dateTimeOriginal', 'timeZone'])])})`])
|
|
||||||
.as('lockedProperties').expression,
|
|
||||||
}))
|
}))
|
||||||
.where('assetId', 'in', ids)
|
.where('assetId', 'in', ids)
|
||||||
.returning(['assetId', 'dateTimeOriginal', 'timeZone'])
|
.returning(['assetId', 'dateTimeOriginal', 'timeZone'])
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`ALTER TABLE "asset_exif" ADD "lockedProperties" character varying[] NOT NULL DEFAULT '{}';`.execute(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await sql`ALTER TABLE "asset_exif" DROP COLUMN "lockedProperties";`.execute(db);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,14 @@ import { AssetTable } from 'src/schema/tables/asset.table';
|
|||||||
import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools';
|
import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools';
|
||||||
|
|
||||||
export type LockableProperty = (typeof lockableProperties)[number];
|
export type LockableProperty = (typeof lockableProperties)[number];
|
||||||
export const lockableProperties = ['description', 'dateTimeOriginal', 'latitude', 'longitude', 'rating'] as const;
|
export const lockableProperties = [
|
||||||
|
'description',
|
||||||
|
'dateTimeOriginal',
|
||||||
|
'latitude',
|
||||||
|
'longitude',
|
||||||
|
'rating',
|
||||||
|
'timeZone',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@Table('asset_exif')
|
@Table('asset_exif')
|
||||||
@UpdatedAtTrigger('asset_exif_updatedAt')
|
@UpdatedAtTrigger('asset_exif_updatedAt')
|
||||||
|
|||||||
Reference in New Issue
Block a user