mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 09:15:44 +03:00
refactor: remove tag entity (#17462)
This commit is contained in:
@@ -92,6 +92,13 @@ export type Asset = {
|
||||
type: AssetType;
|
||||
};
|
||||
|
||||
export type SidecarWriteAsset = {
|
||||
id: string;
|
||||
sidecarPath: string | null;
|
||||
originalPath: string;
|
||||
tags: Array<{ value: string }>;
|
||||
};
|
||||
|
||||
export type AuthSharedLink = {
|
||||
id: string;
|
||||
expiresAt: Date | null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { TagItem } from 'src/types';
|
||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||
|
||||
@@ -52,7 +51,7 @@ export class TagResponseDto {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function mapTag(entity: TagItem | TagEntity): TagResponseDto {
|
||||
export function mapTag(entity: TagItem): TagResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
parentId: entity.parentId ?? undefined,
|
||||
|
||||
@@ -8,11 +8,11 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
import { TagItem } from 'src/types';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
@@ -49,7 +49,7 @@ export class AssetEntity {
|
||||
originalFileName!: string;
|
||||
sidecarPath!: string | null;
|
||||
exifInfo?: ExifEntity;
|
||||
tags!: TagEntity[];
|
||||
tags?: TagItem[];
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
albums?: AlbumEntity[];
|
||||
faces!: AssetFaceEntity[];
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class TagEntity {
|
||||
id!: string;
|
||||
value!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
updateId?: string;
|
||||
color!: string | null;
|
||||
parentId?: string;
|
||||
parent?: TagEntity;
|
||||
children?: TagEntity[];
|
||||
user?: UserEntity;
|
||||
userId!: string;
|
||||
assets?: AssetEntity[];
|
||||
}
|
||||
@@ -210,6 +210,32 @@ where
|
||||
limit
|
||||
$3
|
||||
|
||||
-- AssetRepository.getAssetForSidecarWriteJob
|
||||
select
|
||||
"id",
|
||||
"sidecarPath",
|
||||
"originalPath",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags"."value"
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"assets"."id" = "tag_asset"."assetsId"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = $1::uuid
|
||||
limit
|
||||
$2
|
||||
|
||||
-- AssetRepository.getById
|
||||
select
|
||||
"assets".*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||
@@ -495,6 +496,27 @@ export class AssetRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAssetForSidecarWriteJob(id: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.where('assets.id', '=', asUuid(id))
|
||||
.select((eb) => [
|
||||
'id',
|
||||
'sidecarPath',
|
||||
'originalPath',
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('tags')
|
||||
.select(['tags.value'])
|
||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||
.whereRef('assets.id', '=', 'tag_asset.assetsId'),
|
||||
).as('tags'),
|
||||
])
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(
|
||||
id: string,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { metadataStub } from 'test/fixtures/metadata.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(MetadataService.name, () => {
|
||||
@@ -1405,33 +1406,35 @@ describe(MetadataService.name, () => {
|
||||
|
||||
describe('handleSidecarWrite', () => {
|
||||
it('should skip assets that do not exist anymore', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([]);
|
||||
mocks.asset.getAssetForSidecarWriteJob.mockResolvedValue(void 0);
|
||||
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip jobs with not metadata', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||
it('should skip jobs with no metadata', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
mocks.asset.getAssetForSidecarWriteJob.mockResolvedValue(asset);
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should write tags', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
const description = 'this is a description';
|
||||
const gps = 12;
|
||||
const date = '2023-11-22T04:56:12.196Z';
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||
mocks.asset.getAssetForSidecarWriteJob.mockResolvedValue(asset);
|
||||
await expect(
|
||||
sut.handleSidecarWrite({
|
||||
id: assetStub.sidecar.id,
|
||||
id: asset.id,
|
||||
description,
|
||||
latitude: gps,
|
||||
longitude: gps,
|
||||
dateTimeOriginal: date,
|
||||
}),
|
||||
).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, {
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.sidecarPath, {
|
||||
Description: description,
|
||||
ImageDescription: description,
|
||||
DateTimeOriginal: date,
|
||||
|
||||
@@ -316,7 +316,7 @@ export class MetadataService extends BaseService {
|
||||
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
||||
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
||||
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
||||
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
||||
const asset = await this.assetRepository.getAssetForSidecarWriteJob(id);
|
||||
if (!asset) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user