mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
parent
5dac315af7
commit
a373034629
@ -1,5 +1,6 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { Exif as DatabaseExif } from 'src/db';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetFileType,
|
||||
@ -147,6 +148,7 @@ export type Asset = {
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
sidecarPath: string | null;
|
||||
stack?: Stack | null;
|
||||
stackId: string | null;
|
||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||
type: AssetType;
|
||||
@ -159,6 +161,15 @@ export type SidecarWriteAsset = {
|
||||
tags: Array<{ value: string }>;
|
||||
};
|
||||
|
||||
export type Stack = {
|
||||
id: string;
|
||||
primaryAssetId: string;
|
||||
owner?: User;
|
||||
ownerId: string;
|
||||
assets: AssetEntity[];
|
||||
assetCount?: number;
|
||||
};
|
||||
|
||||
export type AuthSharedLink = {
|
||||
id: string;
|
||||
expiresAt: Date | null;
|
||||
@ -276,7 +287,7 @@ export const columns = {
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
],
|
||||
tagDto: ['id', 'value', 'createdAt', 'updatedAt', 'color', 'parentId'],
|
||||
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||
syncAsset: [
|
||||
'id',
|
||||
@ -292,6 +303,7 @@ export const columns = {
|
||||
'isVisible',
|
||||
'updateId',
|
||||
],
|
||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||
syncAssetExif: [
|
||||
'exif.assetId',
|
||||
'exif.description',
|
||||
@ -320,4 +332,35 @@ export const columns = {
|
||||
'exif.fps',
|
||||
'exif.updateId',
|
||||
],
|
||||
exif: [
|
||||
'exif.assetId',
|
||||
'exif.autoStackId',
|
||||
'exif.bitsPerSample',
|
||||
'exif.city',
|
||||
'exif.colorspace',
|
||||
'exif.country',
|
||||
'exif.dateTimeOriginal',
|
||||
'exif.description',
|
||||
'exif.exifImageHeight',
|
||||
'exif.exifImageWidth',
|
||||
'exif.exposureTime',
|
||||
'exif.fileSizeInByte',
|
||||
'exif.fNumber',
|
||||
'exif.focalLength',
|
||||
'exif.fps',
|
||||
'exif.iso',
|
||||
'exif.latitude',
|
||||
'exif.lensModel',
|
||||
'exif.livePhotoCID',
|
||||
'exif.longitude',
|
||||
'exif.make',
|
||||
'exif.model',
|
||||
'exif.modifyDate',
|
||||
'exif.orientation',
|
||||
'exif.profileDescription',
|
||||
'exif.projectionType',
|
||||
'exif.rating',
|
||||
'exif.state',
|
||||
'exif.timeZone',
|
||||
],
|
||||
} as const;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ArrayMinSize } from 'class-validator';
|
||||
import { Stack } from 'src/database';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { ValidateUUID } from 'src/validation';
|
||||
|
||||
export class StackCreateDto {
|
||||
@ -27,7 +27,7 @@ export class StackResponseDto {
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export const mapStack = (stack: StackEntity, { auth }: { auth?: AuthDto }) => {
|
||||
export const mapStack = (stack: Stack, { auth }: { auth?: AuthDto }) => {
|
||||
const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId);
|
||||
const others = stack.assets.filter((asset) => asset.id !== stack.primaryAssetId);
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { AssetFace, AssetFile, Exif, Tag, User } from 'src/database';
|
||||
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
@ -50,7 +49,7 @@ export class AssetEntity {
|
||||
albums?: AlbumEntity[];
|
||||
faces!: AssetFace[];
|
||||
stackId?: string | null;
|
||||
stack?: StackEntity | null;
|
||||
stack?: Stack | null;
|
||||
jobStatus?: AssetJobStatusEntity;
|
||||
duplicateId!: string | null;
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export class StackEntity {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
assets!: AssetEntity[];
|
||||
primaryAsset!: AssetEntity;
|
||||
primaryAssetId!: string;
|
||||
assetCount?: number;
|
||||
}
|
@ -15,7 +15,35 @@ select
|
||||
"assets"
|
||||
inner join lateral (
|
||||
select
|
||||
"exif".*
|
||||
"exif"."assetId",
|
||||
"exif"."autoStackId",
|
||||
"exif"."bitsPerSample",
|
||||
"exif"."city",
|
||||
"exif"."colorspace",
|
||||
"exif"."country",
|
||||
"exif"."dateTimeOriginal",
|
||||
"exif"."description",
|
||||
"exif"."exifImageHeight",
|
||||
"exif"."exifImageWidth",
|
||||
"exif"."exposureTime",
|
||||
"exif"."fileSizeInByte",
|
||||
"exif"."fNumber",
|
||||
"exif"."focalLength",
|
||||
"exif"."fps",
|
||||
"exif"."iso",
|
||||
"exif"."latitude",
|
||||
"exif"."lensModel",
|
||||
"exif"."livePhotoCID",
|
||||
"exif"."longitude",
|
||||
"exif"."make",
|
||||
"exif"."model",
|
||||
"exif"."modifyDate",
|
||||
"exif"."orientation",
|
||||
"exif"."profileDescription",
|
||||
"exif"."projectionType",
|
||||
"exif"."rating",
|
||||
"exif"."state",
|
||||
"exif"."timeZone"
|
||||
from
|
||||
"exif"
|
||||
where
|
||||
@ -52,7 +80,12 @@ select
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags".*
|
||||
"tags"."id",
|
||||
"tags"."value",
|
||||
"tags"."createdAt",
|
||||
"tags"."updatedAt",
|
||||
"tags"."color",
|
||||
"tags"."parentId"
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
@ -65,7 +98,35 @@ select
|
||||
"assets"
|
||||
inner join lateral (
|
||||
select
|
||||
"exif".*
|
||||
"exif"."assetId",
|
||||
"exif"."autoStackId",
|
||||
"exif"."bitsPerSample",
|
||||
"exif"."city",
|
||||
"exif"."colorspace",
|
||||
"exif"."country",
|
||||
"exif"."dateTimeOriginal",
|
||||
"exif"."description",
|
||||
"exif"."exifImageHeight",
|
||||
"exif"."exifImageWidth",
|
||||
"exif"."exposureTime",
|
||||
"exif"."fileSizeInByte",
|
||||
"exif"."fNumber",
|
||||
"exif"."focalLength",
|
||||
"exif"."fps",
|
||||
"exif"."iso",
|
||||
"exif"."latitude",
|
||||
"exif"."lensModel",
|
||||
"exif"."livePhotoCID",
|
||||
"exif"."longitude",
|
||||
"exif"."make",
|
||||
"exif"."model",
|
||||
"exif"."modifyDate",
|
||||
"exif"."orientation",
|
||||
"exif"."profileDescription",
|
||||
"exif"."projectionType",
|
||||
"exif"."rating",
|
||||
"exif"."state",
|
||||
"exif"."timeZone"
|
||||
from
|
||||
"exif"
|
||||
where
|
||||
|
@ -2,12 +2,12 @@
|
||||
|
||||
-- TagRepository.get
|
||||
select
|
||||
"id",
|
||||
"value",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"color",
|
||||
"parentId"
|
||||
"tags"."id",
|
||||
"tags"."value",
|
||||
"tags"."createdAt",
|
||||
"tags"."updatedAt",
|
||||
"tags"."color",
|
||||
"tags"."parentId"
|
||||
from
|
||||
"tags"
|
||||
where
|
||||
@ -15,12 +15,12 @@ where
|
||||
|
||||
-- TagRepository.getByValue
|
||||
select
|
||||
"id",
|
||||
"value",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"color",
|
||||
"parentId"
|
||||
"tags"."id",
|
||||
"tags"."value",
|
||||
"tags"."createdAt",
|
||||
"tags"."updatedAt",
|
||||
"tags"."color",
|
||||
"tags"."parentId"
|
||||
from
|
||||
"tags"
|
||||
where
|
||||
@ -42,12 +42,12 @@ rollback
|
||||
|
||||
-- TagRepository.getAll
|
||||
select
|
||||
"id",
|
||||
"value",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"color",
|
||||
"parentId"
|
||||
"tags"."id",
|
||||
"tags"."value",
|
||||
"tags"."createdAt",
|
||||
"tags"."updatedAt",
|
||||
"tags"."color",
|
||||
"tags"."parentId"
|
||||
from
|
||||
"tags"
|
||||
where
|
||||
|
@ -2,9 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Kysely, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { columns } from 'src/database';
|
||||
import { AssetStack, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export interface StackSearch {
|
||||
@ -18,7 +19,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.innerJoinLateral(
|
||||
(eb) => eb.selectFrom('exif').selectAll('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
|
||||
(eb) => eb.selectFrom('exif').select(columns.exif).whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.$if(withTags, (eb) =>
|
||||
@ -26,7 +27,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('tags')
|
||||
.selectAll('tags')
|
||||
.select(columns.tag)
|
||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||
.whereRef('tag_asset.assetsId', '=', 'assets.id'),
|
||||
).as('tags'),
|
||||
@ -35,7 +36,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
).as('assets');
|
||||
)
|
||||
.$castTo<AssetEntity[]>()
|
||||
.as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -43,17 +46,17 @@ export class StackRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
|
||||
search(query: StackSearch): Promise<StackEntity[]> {
|
||||
search(query: StackSearch) {
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('asset_stack.ownerId', '=', query.ownerId)
|
||||
.$if(!!query.primaryAssetId, (eb) => eb.where('asset_stack.primaryAssetId', '=', query.primaryAssetId!))
|
||||
.execute() as unknown as Promise<StackEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async create(entity: { ownerId: string; assetIds: string[] }): Promise<StackEntity> {
|
||||
async create(entity: { ownerId: string; assetIds: string[] }) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const stacks = await tx
|
||||
.selectFrom('asset_stack')
|
||||
@ -116,7 +119,7 @@ export class StackRepository {
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('id', '=', newRecord.id)
|
||||
.executeTakeFirst() as unknown as Promise<StackEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
@ -129,23 +132,23 @@ export class StackRepository {
|
||||
await this.db.deleteFrom('asset_stack').where('id', 'in', ids).execute();
|
||||
}
|
||||
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity> {
|
||||
update(id: string, entity: Updateable<AssetStack>) {
|
||||
return this.db
|
||||
.updateTable('asset_stack')
|
||||
.set(entity)
|
||||
.where('id', '=', asUuid(id))
|
||||
.returningAll('asset_stack')
|
||||
.returning((eb) => withAssets(eb, true))
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<StackEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(id: string): Promise<StackEntity | undefined> {
|
||||
getById(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll()
|
||||
.select((eb) => withAssets(eb, true))
|
||||
.where('id', '=', asUuid(id))
|
||||
.executeTakeFirst() as Promise<StackEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
|
@ -17,14 +17,14 @@ export class TagRepository {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string) {
|
||||
return this.db.selectFrom('tags').select(columns.tagDto).where('id', '=', id).executeTakeFirst();
|
||||
return this.db.selectFrom('tags').select(columns.tag).where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
getByValue(userId: string, value: string) {
|
||||
return this.db
|
||||
.selectFrom('tags')
|
||||
.select(columns.tagDto)
|
||||
.select(columns.tag)
|
||||
.where('userId', '=', userId)
|
||||
.where('value', '=', value)
|
||||
.executeTakeFirst();
|
||||
@ -68,12 +68,7 @@ export class TagRepository {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAll(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('tags')
|
||||
.select(columns.tagDto)
|
||||
.where('userId', '=', userId)
|
||||
.orderBy('value asc')
|
||||
.execute();
|
||||
return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value asc').execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, color: DummyValue.STRING, value: DummyValue.STRING }] })
|
||||
|
@ -592,8 +592,8 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
||||
mocks.stack.update.mockResolvedValue(factory.stack() as unknown as any);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||
mocks.stack.update.mockResolvedValue(factory.stack() as any);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
|
@ -203,7 +203,7 @@ export class AssetService extends BaseService {
|
||||
|
||||
// Replace the parent of the stack children with a new asset
|
||||
if (asset.stack?.primaryAssetId === id) {
|
||||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
const stackAssetIds = asset.stack?.assets.map((a) => a.id) ?? [];
|
||||
if (stackAssetIds.length > 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
await this.stackRepository.update(asset.stack.id, {
|
||||
|
3
server/test/fixtures/asset.stub.ts
vendored
3
server/test/fixtures/asset.stub.ts
vendored
@ -1,6 +1,5 @@
|
||||
import { AssetFile, Exif } from 'src/database';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import { StorageAsset } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
@ -27,7 +26,7 @@ const fullsizeFile: AssetFile = {
|
||||
|
||||
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
|
||||
|
||||
export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => {
|
||||
export const stackStub = (stackId: string, assets: AssetEntity[]) => {
|
||||
return {
|
||||
id: stackId,
|
||||
assets,
|
||||
|
Loading…
x
Reference in New Issue
Block a user