refactor: move asset stacks to their own entity (#6353)

* feat: auto-stack burst photos

* feat: move stacks to asset stack entity

* chore: pin node version with volta in server

* chore: update e2e cases

* chore: cleanup

* feat: migrate existing stacks

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Zack Pollard 2024-01-27 18:52:14 +00:00 committed by GitHub
parent c4b8c853bc
commit 25cad79657
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 538 additions and 243 deletions

View File

@ -10,7 +10,7 @@ import {
usePagination,
} from '@app/domain';
import { AssetController } from '@app/immich';
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities';
import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/infra/entities';
import { AssetRepository } from '@app/infra/repositories';
import { INestApplication } from '@nestjs/common';
import { errorStub, userDto, uuidStub } from '@test/fixtures';
@ -94,32 +94,7 @@ describe(`${AssetController.name} (e2e)`, () => {
});
beforeEach(async () => {
await testApp.reset({ entities: [AssetEntity] });
[asset1, asset2, asset3, asset4, asset5] = await Promise.all([
createAsset(user1, new Date('1970-01-01')),
createAsset(user1, new Date('1970-02-10')),
createAsset(user1, new Date('1970-02-11'), {
isFavorite: true,
isArchived: true,
isExternal: true,
isReadOnly: true,
type: AssetType.VIDEO,
fileCreatedAt: yesterday.toJSDate(),
fileModifiedAt: yesterday.toJSDate(),
createdAt: yesterday.toJSDate(),
updatedAt: yesterday.toJSDate(),
localDateTime: yesterday.toJSDate(),
}),
createAsset(user2, new Date('1970-01-01')),
createAsset(user1, new Date('1970-01-01'), {
deletedAt: yesterday.toJSDate(),
}),
]);
});
beforeEach(async () => {
await testApp.reset({ entities: [AssetEntity] });
await testApp.reset({ entities: [AssetEntity, AssetStackEntity] });
[asset1, asset2, asset3, asset4, asset5] = await Promise.all([
createAsset(user1, new Date('1970-01-01')),
@ -571,11 +546,7 @@ describe(`${AssetController.name} (e2e)`, () => {
.get(`/asset/assetById/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
expect(body).toMatchObject({ id: asset1.id });
});
it('should work with a shared link', async () => {
@ -586,11 +557,7 @@ describe(`${AssetController.name} (e2e)`, () => {
const { status, body } = await request(server).get(`/asset/assetById/${asset1.id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
expect(body).toMatchObject({ id: asset1.id });
});
});
@ -622,11 +589,7 @@ describe(`${AssetController.name} (e2e)`, () => {
.get(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
expect(body).toMatchObject({ id: asset1.id });
});
it('should work with a shared link', async () => {
@ -637,11 +600,7 @@ describe(`${AssetController.name} (e2e)`, () => {
const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: asset1.id,
stack: [],
stackCount: 0,
});
expect(body).toMatchObject({ id: asset1.id });
});
});
@ -1371,7 +1330,7 @@ describe(`${AssetController.name} (e2e)`, () => {
expect(status).toBe(204);
const asset = await api.assetApi.get(server, user1.accessToken, asset1.id);
expect(asset.stack).toHaveLength(0);
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {

View File

@ -30,6 +30,9 @@ export const db = {
.map((entity) => entity.tableName)
.filter((tableName) => !tableName.startsWith('geodata'));
if (tableNames.includes('asset_stack')) {
await em.query(`DELETE FROM "asset_stack" CASCADE;`);
}
let deleteUsers = false;
for (const tableName of tableNames) {
if (tableName === 'users') {

View File

@ -156,5 +156,8 @@
"^@app/domain(|/.*)$": "<rootDir>/src/domain/$1"
},
"globalSetup": "<rootDir>/test/global-setup.js"
},
"volta": {
"node": "20.11.0"
}
}

View File

@ -2,11 +2,13 @@ import { AssetEntity, AssetType } from '@app/infra/entities';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import {
IAccessRepositoryMock,
assetStackStub,
assetStub,
authStub,
faceStub,
newAccessRepositoryMock,
newAssetRepositoryMock,
newAssetStackRepositoryMock,
newCommunicationRepositoryMock,
newJobRepositoryMock,
newPartnerRepositoryMock,
@ -20,6 +22,7 @@ import {
AssetStats,
ClientEvent,
IAssetRepository,
IAssetStackRepository,
ICommunicationRepository,
IJobRepository,
IPartnerRepository,
@ -160,6 +163,7 @@ describe(AssetService.name, () => {
let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>;
let assetStackMock: jest.Mocked<IAssetStackRepository>;
it('should work', () => {
expect(sut).toBeDefined();
@ -174,6 +178,7 @@ describe(AssetService.name, () => {
userMock = newUserRepositoryMock();
configMock = newSystemConfigRepositoryMock();
partnerMock = newPartnerRepositoryMock();
assetStackMock = newAssetStackRepositoryMock();
sut = new AssetService(
accessMock,
@ -184,6 +189,7 @@ describe(AssetService.name, () => {
userMock,
communicationMock,
partnerMock,
assetStackMock,
);
when(assetMock.getById)
@ -578,65 +584,121 @@ describe(AssetService.name, () => {
).rejects.toBeInstanceOf(BadRequestException);
});
it('should update parent asset when children are added', async () => {
it('should update parent asset updatedAt when children are added', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['parent']));
when(assetMock.getById)
.calledWith('parent', { stack: { assets: true } })
.mockResolvedValue(assetStub.image);
await sut.updateAll(authStub.user1, {
ids: [],
stackParentId: 'parent',
}),
expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { stackParentId: null });
expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { updatedAt: expect.any(Date) });
});
it('should update parent asset when children are removed', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1']));
assetMock.getByIds.mockResolvedValue([{ id: 'child-1', stackParentId: 'parent' } as AssetEntity]);
assetMock.getByIds.mockResolvedValue([
{
id: 'child-1',
stackId: 'stack-1',
stack: assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity, { id: 'child-1' } as AssetEntity]),
} as AssetEntity,
]);
when(assetStackMock.getById)
.calledWith('stack-1')
.mockResolvedValue(assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
await sut.updateAll(authStub.user1, {
ids: ['child-1'],
removeParent: true,
}),
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { stackParentId: null });
});
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['child-1']), { stack: null });
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), {
updatedAt: expect.any(Date),
});
expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1');
});
it('update parentId for new children', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
const stack = assetStackStub('stack-1', [
{ id: 'parent' } as AssetEntity,
{ id: 'child-1' } as AssetEntity,
{ id: 'child-2' } as AssetEntity,
]);
when(assetMock.getById)
.calledWith('parent', { stack: { assets: true } })
.mockResolvedValue({
id: 'child-1',
stack,
} as AssetEntity);
await sut.updateAll(authStub.user1, {
stackParentId: 'parent',
ids: ['child-1', 'child-2'],
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
expect(assetStackMock.update).toHaveBeenCalledWith({
...assetStackStub('stack-1', [
{ id: 'child-1' } as AssetEntity,
{ id: 'child-2' } as AssetEntity,
{ id: 'parent' } as AssetEntity,
]),
primaryAsset: undefined,
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2', 'parent'], { updatedAt: expect.any(Date) });
});
it('nullify parentId for remove children', async () => {
it('remove stack for removed children', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1', 'child-2']));
await sut.updateAll(authStub.user1, {
removeParent: true,
ids: ['child-1', 'child-2'],
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: null });
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stack: null });
});
it('merge stacks if new child has children', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
when(assetMock.getById)
.calledWith('parent', { stack: { assets: true } })
.mockResolvedValue({ ...assetStub.image, id: 'parent' });
assetMock.getByIds.mockResolvedValue([
{ id: 'child-1', stack: [{ id: 'child-2' } as AssetEntity] } as AssetEntity,
{
id: 'child-1',
stackId: 'stack-1',
stack: assetStackStub('stack-1', [{ id: 'child-1' } as AssetEntity, { id: 'child-2' } as AssetEntity]),
} as AssetEntity,
]);
when(assetStackMock.getById)
.calledWith('stack-1')
.mockResolvedValue(assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
await sut.updateAll(authStub.user1, {
ids: ['child-1'],
stackParentId: 'parent',
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1');
expect(assetStackMock.create).toHaveBeenCalledWith({
assets: [{ id: 'child-1' }, { id: 'parent' }, { id: 'child-1' }, { id: 'child-2' }],
primaryAssetId: 'parent',
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], {
updatedAt: expect.any(Date),
});
});
it('should send ws asset update event', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-1']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
when(assetMock.getById)
.calledWith('parent', { stack: { assets: true } })
.mockResolvedValue(assetStub.image);
await sut.updateAll(authStub.user1, {
ids: ['asset-1'],
@ -645,6 +707,7 @@ describe(AssetService.name, () => {
expect(communicationMock.send).toHaveBeenCalledWith(ClientEvent.ASSET_UPDATE, authStub.user1.user.id, [
'asset-1',
'parent',
]);
});
});
@ -702,7 +765,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetWithFace);
@ -729,25 +792,23 @@ describe(AssetService.name, () => {
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
});
it('should update stack parent if asset has stack children', async () => {
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
when(assetMock.getById)
.calledWith(assetStub.primaryImage.id, {
faces: {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.primaryImage);
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-2'], {
stackParentId: 'stack-child-asset-1',
});
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-1'], {
stackParentId: null,
expect(assetStackMock.update).toHaveBeenCalledWith({
id: 'stack-1',
primaryAssetId: 'stack-child-asset-1',
});
});
@ -758,7 +819,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.readOnly);
@ -787,7 +848,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.external);
@ -819,7 +880,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.livePhotoStillAsset);
@ -829,7 +890,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.livePhotoMotionAsset);
@ -864,7 +925,7 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
})
.mockResolvedValue(assetStub.image);
@ -927,54 +988,21 @@ describe(AssetService.name, () => {
person: true,
},
library: true,
stack: true,
stack: {
assets: true,
},
})
.mockResolvedValue(assetStub.image as AssetEntity);
.mockResolvedValue({ ...assetStub.image, stackId: 'stack-1' });
await sut.updateStackParent(authStub.user1, {
oldParentId: assetStub.image.id,
newParentId: 'new',
});
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id], { stackParentId: 'new' });
expect(assetStackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' });
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], {
updatedAt: expect.any(Date),
});
it('remove stackParentId of new parent', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id]));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
await sut.updateStackParent(authStub.user1, {
oldParentId: assetStub.primaryImage.id,
newParentId: 'new',
});
expect(assetMock.updateAll).toBeCalledWith(['new'], { stackParentId: null });
});
it('update stackParentId of old parents children to new parent', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id]));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
when(assetMock.getById)
.calledWith(assetStub.primaryImage.id, {
faces: {
person: true,
},
library: true,
stack: true,
})
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
await sut.updateStackParent(authStub.user1, {
oldParentId: assetStub.primaryImage.id,
newParentId: 'new',
});
expect(assetMock.updateAll).toBeCalledWith(
[assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'],
{
stackParentId: 'new',
},
);
});
});

View File

@ -14,6 +14,7 @@ import {
ClientEvent,
IAccessRepository,
IAssetRepository,
IAssetStackRepository,
ICommunicationRepository,
IJobRepository,
IPartnerRepository,
@ -85,6 +86,7 @@ export class AssetService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
) {
this.access = AccessCore.create(accessRepository);
this.configCore = SystemConfigCore.create(configRepository);
@ -299,8 +301,10 @@ export class AssetService {
person: true,
},
stack: {
assets: {
exifInfo: true,
},
},
});
if (!asset) {
@ -338,25 +342,51 @@ export class AssetService {
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids);
// TODO: refactor this logic into separate API calls POST /stack, PUT /stack, etc.
const stackIdsToCheckForDelete: string[] = [];
if (removeParent) {
(options as Partial<AssetEntity>).stackParentId = null;
(options as Partial<AssetEntity>).stack = null;
const assets = await this.assetRepository.getByIds(ids);
stackIdsToCheckForDelete.push(...new Set(assets.filter((a) => !!a.stackId).map((a) => a.stackId!)));
// This updates the updatedAt column of the parents to indicate that one of its children is removed
// All the unique parent's -> parent is set to null
ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!)));
await this.assetRepository.updateAll(
assets.filter((a) => !!a.stack?.primaryAssetId).map((a) => a.stack!.primaryAssetId!),
{ updatedAt: new Date() },
);
} else if (options.stackParentId) {
//Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId);
// Merge stacks
const assets = await this.assetRepository.getByIds(ids);
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0);
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.map((gChild) => gChild.id)));
const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } });
if (!primaryAsset) {
throw new BadRequestException('Asset not found for given stackParentId');
}
let stack = primaryAsset.stack;
// This updates the updatedAt column of the parent to indicate that a new child has been added
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
ids.push(options.stackParentId);
const assets = await this.assetRepository.getByIds(ids, { stack: { assets: true } });
stackIdsToCheckForDelete.push(
...new Set(assets.filter((a) => !!a.stackId && stack?.id !== a.stackId).map((a) => a.stackId!)),
);
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0);
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id)));
if (!stack) {
stack = await this.assetStackRepository.create({
primaryAssetId: primaryAsset.id,
assets: ids.map((id) => ({ id }) as AssetEntity),
});
} else {
await this.assetStackRepository.update({
id: stack.id,
primaryAssetId: primaryAsset.id,
assets: ids.map((id) => ({ id }) as AssetEntity),
});
}
for (const id of ids) {
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
// Merge stacks
options.stackParentId = undefined;
(options as Partial<AssetEntity>).updatedAt = new Date();
}
for (const id of ids) {
@ -364,6 +394,12 @@ export class AssetService {
}
await this.assetRepository.updateAll(ids, options);
const stacksToDelete = (
await Promise.all(stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)))
)
.flatMap((stack) => (stack ? [stack] : []))
.filter((stack) => stack.assets.length < 2);
await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id)));
this.communicationRepository.send(ClientEvent.ASSET_UPDATE, auth.user.id, ids);
}
@ -394,7 +430,7 @@ export class AssetService {
person: true,
},
library: true,
stack: true,
stack: { assets: true },
exifInfo: true,
});
@ -408,11 +444,17 @@ export class AssetService {
}
// Replace the parent of the stack children with a new asset
if (asset.stack && asset.stack.length != 0) {
const stackIds = asset.stack.map((a) => a.id);
const newParentId = stackIds[0];
await this.assetRepository.updateAll(stackIds.slice(1), { stackParentId: newParentId });
await this.assetRepository.updateAll([newParentId], { stackParentId: null });
if (asset.stack?.primaryAssetId === id) {
const stackAssetIds = asset.stack.assets.map((a) => a.id);
if (stackAssetIds.length > 2) {
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
await this.assetStackRepository.update({
id: asset.stack.id,
primaryAssetId: newPrimaryAssetId,
});
} else {
await this.assetStackRepository.delete(asset.stack.id);
}
}
await this.assetRepository.remove(asset);
@ -460,18 +502,25 @@ export class AssetService {
person: true,
},
library: true,
stack: true,
stack: {
assets: true,
},
});
if (!oldParent?.stackId) {
throw new Error('Asset not found or not in a stack');
}
if (oldParent != null) {
childIds.push(oldParent.id);
// Get all children of old parent
childIds.push(...(oldParent.stack?.map((a) => a.id) ?? []));
childIds.push(...(oldParent.stack?.assets.map((a) => a.id) ?? []));
}
await this.assetStackRepository.update({
id: oldParent.stackId,
primaryAssetId: newParentId,
});
this.communicationRepository.send(ClientEvent.ASSET_UPDATE, auth.user.id, [...childIds, newParentId]);
await this.assetRepository.updateAll(childIds, { stackParentId: newParentId });
// Remove ParentId of new parent if this was previously a child of some other asset
return this.assetRepository.updateAll([newParentId], { stackParentId: null });
this.communicationRepository.send(ClientEvent.ASSET_UPDATE, auth.user.id, [...childIds, newParentId, oldParentId]);
await this.assetRepository.updateAll([oldParentId, newParentId, ...childIds], { updatedAt: new Date() });
}
async run(auth: AuthDto, dto: AssetJobsDto) {

View File

@ -116,9 +116,13 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
tags: entity.tags?.map(mapTag),
people: peopleWithFaces(entity.faces),
checksum: entity.checksum.toString('base64'),
stackParentId: entity.stackParentId,
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
stackCount: entity.stack?.length ?? null,
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
stack: withStack
? entity.stack?.assets
.filter((a) => a.id !== entity.stack?.primaryAssetId)
.map((a) => mapAsset(a, { stripMetadata }))
: undefined,
stackCount: entity.stack?.assets?.length ?? null,
isExternal: entity.isExternal,
isOffline: entity.isOffline,
isReadOnly: entity.isReadOnly,

View File

@ -499,6 +499,7 @@ describe(MetadataService.name, () => {
expect(assetMock.upsertExif).toHaveBeenCalledWith({
assetId: assetStub.image.id,
bitsPerSample: expect.any(Number),
autoStackId: null,
colorspace: tags.ColorSpace,
dateTimeOriginal: new Date('1970-01-01'),
description: tags.ImageDescription,

View File

@ -499,6 +499,7 @@ export class MetadataService {
latitude: validate(tags.GPSLatitude),
lensModel: tags.LensModel ?? null,
livePhotoCID: (tags.ContentIdentifier || tags.MediaGroupUUID) ?? null,
autoStackId: this.getAutoStackId(tags),
longitude: validate(tags.GPSLongitude),
make: tags.Make ?? null,
model: tags.Model ?? null,
@ -518,6 +519,13 @@ export class MetadataService {
return { exifData, tags };
}
private getAutoStackId(tags: ImmichTags | null): string | null {
if (!tags) {
return null;
}
return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null;
}
private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
if (!tags) {
return null;

View File

@ -0,0 +1,10 @@
import { AssetStackEntity } from '@app/infra/entities/asset-stack.entity';
export const IAssetStackRepository = 'IAssetStackRepository';
export interface IAssetStackRepository {
create(assetStack: Partial<AssetStackEntity>): Promise<AssetStackEntity>;
update(asset: Pick<AssetStackEntity, 'id'> & Partial<AssetStackEntity>): Promise<AssetStackEntity>;
delete(id: string): Promise<void>;
getById(id: string): Promise<AssetStackEntity | null>;
}

View File

@ -2,6 +2,7 @@ export * from './access.repository';
export * from './activity.repository';
export * from './album.repository';
export * from './api-key.repository';
export * from './asset-stack.repository';
export * from './asset.repository';
export * from './audit.repository';
export * from './communication.repository';

View File

@ -108,7 +108,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
relations: {
exifInfo: true,
tags: true,
stack: true,
stack: { assets: true },
},
skip: dto.skip || 0,
take: dto.take,

View File

@ -0,0 +1,19 @@
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AssetEntity } from './asset.entity';
@Entity('asset_stack')
export class AssetStackEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@OneToMany(() => AssetEntity, (asset) => asset.stack)
assets!: AssetEntity[];
@OneToOne(() => AssetEntity)
@JoinColumn()
//TODO: Add constraint to ensure primary asset exists in the assets array
primaryAsset!: AssetEntity;
@Column({ nullable: false })
primaryAssetId!: string;
}

View File

@ -16,6 +16,7 @@ import {
import { AlbumEntity } from './album.entity';
import { AssetFaceEntity } from './asset-face.entity';
import { AssetJobStatusEntity } from './asset-job-status.entity';
import { AssetStackEntity } from './asset-stack.entity';
import { ExifEntity } from './exif.entity';
import { LibraryEntity } from './library.entity';
import { SharedLinkEntity } from './shared-link.entity';
@ -34,7 +35,6 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
@Index('IDX_day_of_month', { synchronize: false })
@Index('IDX_month', { synchronize: false })
@Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId'])
@Index(['stackParentId'])
// For all assets, each originalpath must be unique per user and library
export class AssetEntity {
@PrimaryGeneratedColumn('uuid')
@ -157,14 +157,11 @@ export class AssetEntity {
faces!: AssetFaceEntity[];
@Column({ nullable: true })
stackParentId?: string | null;
stackId?: string | null;
@ManyToOne(() => AssetEntity, (asset) => asset.stack, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'stackParentId' })
stackParent?: AssetEntity | null;
@OneToMany(() => AssetEntity, (asset) => asset.stackParent)
stack?: AssetEntity[];
@ManyToOne(() => AssetStackEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
@JoinColumn()
stack?: AssetStackEntity | null;
@OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true })
jobStatus?: AssetJobStatusEntity;

View File

@ -54,6 +54,10 @@ export class ExifEntity {
@Column({ type: 'varchar', nullable: true })
livePhotoCID!: string | null;
@Index('IDX_auto_stack_id')
@Column({ type: 'varchar', nullable: true })
autoStackId!: string | null;
@Column({ type: 'varchar', nullable: true })
state!: string | null;

View File

@ -1,13 +1,14 @@
import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity';
import { ActivityEntity } from './activity.entity';
import { AlbumEntity } from './album.entity';
import { APIKeyEntity } from './api-key.entity';
import { AssetFaceEntity } from './asset-face.entity';
import { AssetJobStatusEntity } from './asset-job-status.entity';
import { AssetStackEntity } from './asset-stack.entity';
import { AssetEntity } from './asset.entity';
import { AuditEntity } from './audit.entity';
import { ExifEntity } from './exif.entity';
import { GeodataAdmin1Entity } from './geodata-admin1.entity';
import { GeodataAdmin2Entity } from './geodata-admin2.entity';
import { GeodataPlacesEntity } from './geodata-places.entity';
import { LibraryEntity } from './library.entity';
import { MoveEntity } from './move.entity';
@ -27,6 +28,7 @@ export * from './album.entity';
export * from './api-key.entity';
export * from './asset-face.entity';
export * from './asset-job-status.entity';
export * from './asset-stack.entity';
export * from './asset.entity';
export * from './audit.entity';
export * from './exif.entity';
@ -51,6 +53,7 @@ export const databaseEntities = [
AlbumEntity,
APIKeyEntity,
AssetEntity,
AssetStackEntity,
AssetFaceEntity,
AssetJobStatusEntity,
AuditEntity,

View File

@ -3,6 +3,7 @@ import {
IActivityRepository,
IAlbumRepository,
IAssetRepository,
IAssetStackRepository,
IAuditRepository,
ICommunicationRepository,
ICryptoRepository,
@ -41,6 +42,7 @@ import {
AlbumRepository,
ApiKeyRepository,
AssetRepository,
AssetStackRepository,
AuditRepository,
CommunicationRepository,
CryptoRepository,
@ -69,6 +71,7 @@ const providers: Provider[] = [
{ provide: IAccessRepository, useClass: AccessRepository },
{ provide: IAlbumRepository, useClass: AlbumRepository },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: IAssetStackRepository, useClass: AssetStackRepository },
{ provide: IAuditRepository, useClass: AuditRepository },
{ provide: ICommunicationRepository, useClass: CommunicationRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository },

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAutoStackId1703035138085 implements MigrationInterface {
name = 'AddAutoStackId1703035138085'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ADD "autoStackId" character varying`);
await queryRunner.query(`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_auto_stack_id"`);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "autoStackId"`);
}
}

View File

@ -0,0 +1,83 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateAssetStackTable1705197515600 implements MigrationInterface {
name = 'CreateAssetStackTable1705197515600';
public async up(queryRunner: QueryRunner): Promise<void> {
// create table
await queryRunner.query(
`CREATE TABLE "asset_stack" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"primaryAssetId" uuid NOT NULL,
CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId"),
CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id"))`,
);
// create stacks
await queryRunner.query(
`INSERT INTO "asset_stack" ("primaryAssetId")
SELECT DISTINCT("stackParentId" )
FROM "assets"
WHERE "stackParentId" IS NOT NULL;`,
);
// add "stackId"
await queryRunner.query(`ALTER TABLE "assets" ADD COLUMN "stackId" uuid`);
// set "stackId" for parents
await queryRunner.query(
`UPDATE "assets"
SET "stackId" = "asset_stack"."id"
FROM "asset_stack"
WHERE "assets"."id" = "asset_stack"."primaryAssetId"`,
);
// set "stackId" for children
await queryRunner.query(
`UPDATE "assets"
SET "stackId" = "asset_stack"."id"
FROM "asset_stack"
WHERE "assets"."stackParentId" = "asset_stack"."primaryAssetId"`,
);
// update constraints
await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`);
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`);
await queryRunner.query(
`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack"("id") ON DELETE SET NULL ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
// drop "stackParentId"
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "stackParentId"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// add "stackParentId"
await queryRunner.query(`ALTER TABLE "assets" ADD COLUMN "stackParentId" uuid`);
// set "stackParentId" for parents
await queryRunner.query(
`UPDATE "assets"
SET "stackParentId" = "asset_stack"."primaryAssetId"
FROM "asset_stack"
WHERE "assets"."stackId" = "asset_stack"."id" and "assets"."id" != "asset_stack"."primaryAssetId"`,
);
// update constraints
await queryRunner.query(
`ALTER TABLE "assets" ADD CONSTRAINT "FK_b463c8edb01364bf2beba08ef19" FOREIGN KEY ("stackParentId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`,
);
await queryRunner.query(`CREATE INDEX "IDX_b463c8edb01364bf2beba08ef1" ON "assets" ("stackParentId") `);
await queryRunner.query(`ALTER TABLE "asset_stack" DROP CONSTRAINT "FK_91704e101438fd0653f582426dc"`);
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207"`);
// drop table
await queryRunner.query(`DROP TABLE "asset_stack"`);
// drop "stackId"
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "stackId"`);
}
}

View File

@ -0,0 +1,47 @@
import { IAssetStackRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AssetStackEntity } from '../entities';
@Injectable()
export class AssetStackRepository implements IAssetStackRepository {
constructor(@InjectRepository(AssetStackEntity) private repository: Repository<AssetStackEntity>) {}
create(entity: Partial<AssetStackEntity>) {
return this.save(entity);
}
async delete(id: string): Promise<void> {
await this.repository.delete(id);
}
update(entity: Partial<AssetStackEntity>) {
return this.save(entity);
}
async getById(id: string): Promise<AssetStackEntity | null> {
return this.repository.findOne({
where: {
id,
},
relations: {
primaryAsset: true,
assets: true,
},
});
}
private async save(entity: Partial<AssetStackEntity>) {
const { id } = await this.repository.save(entity);
return this.repository.findOneOrFail({
where: {
id,
},
relations: {
primaryAsset: true,
assets: true,
},
});
}
}

View File

@ -138,7 +138,7 @@ export class AssetRepository implements IAssetRepository {
const withExif = Object.keys(exifWhere).length > 0 || _withExif;
const where = _.omitBy(
const where: FindOptionsWhere<AssetEntity> = _.omitBy(
{
ownerId,
id,
@ -182,10 +182,6 @@ export class AssetRepository implements IAssetRepository {
builder.leftJoinAndSelect('faces.person', 'person');
}
if (withStacked) {
builder.leftJoinAndSelect('asset.stack', 'stack');
}
if (withSmartInfo) {
builder.leftJoinAndSelect('asset.smartInfo', 'smartInfo');
}
@ -194,13 +190,20 @@ export class AssetRepository implements IAssetRepository {
builder.withDeleted();
}
builder.where(where);
if (withStacked) {
builder
.where(where)
.leftJoinAndSelect('asset.stack', 'stack')
.leftJoinAndSelect('stack.assets', 'stackedAssets')
.andWhere(new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL')));
}
return builder
.skip(size * (page - 1))
.take(size)
.orderBy('asset.fileCreatedAt', order ?? 'DESC');
return builder.getMany();
.orderBy('asset.fileCreatedAt', order ?? 'DESC')
.getMany();
}
create(asset: AssetCreate): Promise<AssetEntity> {
@ -279,7 +282,9 @@ export class AssetRepository implements IAssetRepository {
faces: {
person: true,
},
stack: true,
stack: {
assets: true,
},
};
}
@ -797,8 +802,14 @@ export class AssetRepository implements IAssetRepository {
builder = builder.andWhere('asset.type = :assetType', { assetType });
}
let stackJoined = false;
if (exifInfo !== false) {
builder = builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo').leftJoinAndSelect('asset.stack', 'stack');
stackJoined = true;
builder = builder
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.leftJoinAndSelect('stack.assets', 'stackedAssets');
}
if (albumId) {
@ -829,7 +840,12 @@ export class AssetRepository implements IAssetRepository {
}
if (withStacked) {
builder = builder.andWhere('asset.stackParentId IS NULL');
if (!stackJoined) {
builder = builder.leftJoinAndSelect('asset.stack', 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets');
}
builder = builder.andWhere(
new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL')),
);
}
return builder;

View File

@ -2,6 +2,7 @@ export * from './access.repository';
export * from './activity.repository';
export * from './album.repository';
export * from './api-key.repository';
export * from './asset-stack.repository';
export * from './asset.repository';
export * from './audit.repository';
export * from './communication.repository';

View File

@ -30,7 +30,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
@ -45,6 +45,7 @@ SELECT
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
@ -105,7 +106,7 @@ SELECT
"entity"."livePhotoVideoId" AS "entity_livePhotoVideoId",
"entity"."originalFileName" AS "entity_originalFileName",
"entity"."sidecarPath" AS "entity_sidecarPath",
"entity"."stackParentId" AS "entity_stackParentId",
"entity"."stackId" AS "entity_stackId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -120,6 +121,7 @@ SELECT
"exifInfo"."projectionType" AS "exifInfo_projectionType",
"exifInfo"."city" AS "exifInfo_city",
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
"exifInfo"."state" AS "exifInfo_state",
"exifInfo"."country" AS "exifInfo_country",
"exifInfo"."make" AS "exifInfo_make",
@ -187,7 +189,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
@ -202,6 +204,7 @@ SELECT
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
@ -242,34 +245,36 @@ SELECT
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
"AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId",
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
"AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId",
"AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId",
"AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type",
"AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath",
"AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath",
"AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath",
"AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash",
"AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath",
"AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt",
"AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt",
"AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt",
"AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt",
"AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime",
"AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt",
"AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite",
"AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived",
"AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal",
"AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly",
"AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline",
"AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum",
"AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration",
"AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible",
"AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId",
"AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName",
"AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath",
"AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId"
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId",
"bd93d5747511a4dad4923546c51365bf1a803774"."id" AS "bd93d5747511a4dad4923546c51365bf1a803774_id",
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceAssetId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceAssetId",
"bd93d5747511a4dad4923546c51365bf1a803774"."ownerId" AS "bd93d5747511a4dad4923546c51365bf1a803774_ownerId",
"bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId",
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
"bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
"bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."resizePath" AS "bd93d5747511a4dad4923546c51365bf1a803774_resizePath",
"bd93d5747511a4dad4923546c51365bf1a803774"."webpPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_webpPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
"bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt",
"bd93d5747511a4dad4923546c51365bf1a803774"."updatedAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_updatedAt",
"bd93d5747511a4dad4923546c51365bf1a803774"."deletedAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_deletedAt",
"bd93d5747511a4dad4923546c51365bf1a803774"."fileCreatedAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_fileCreatedAt",
"bd93d5747511a4dad4923546c51365bf1a803774"."localDateTime" AS "bd93d5747511a4dad4923546c51365bf1a803774_localDateTime",
"bd93d5747511a4dad4923546c51365bf1a803774"."fileModifiedAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_fileModifiedAt",
"bd93d5747511a4dad4923546c51365bf1a803774"."isFavorite" AS "bd93d5747511a4dad4923546c51365bf1a803774_isFavorite",
"bd93d5747511a4dad4923546c51365bf1a803774"."isArchived" AS "bd93d5747511a4dad4923546c51365bf1a803774_isArchived",
"bd93d5747511a4dad4923546c51365bf1a803774"."isExternal" AS "bd93d5747511a4dad4923546c51365bf1a803774_isExternal",
"bd93d5747511a4dad4923546c51365bf1a803774"."isReadOnly" AS "bd93d5747511a4dad4923546c51365bf1a803774_isReadOnly",
"bd93d5747511a4dad4923546c51365bf1a803774"."isOffline" AS "bd93d5747511a4dad4923546c51365bf1a803774_isOffline",
"bd93d5747511a4dad4923546c51365bf1a803774"."checksum" AS "bd93d5747511a4dad4923546c51365bf1a803774_checksum",
"bd93d5747511a4dad4923546c51365bf1a803774"."duration" AS "bd93d5747511a4dad4923546c51365bf1a803774_duration",
"bd93d5747511a4dad4923546c51365bf1a803774"."isVisible" AS "bd93d5747511a4dad4923546c51365bf1a803774_isVisible",
"bd93d5747511a4dad4923546c51365bf1a803774"."livePhotoVideoId" AS "bd93d5747511a4dad4923546c51365bf1a803774_livePhotoVideoId",
"bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName",
"bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId"
FROM
"assets" "AssetEntity"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
@ -278,7 +283,8 @@ FROM
LEFT JOIN "tags" "AssetEntity__AssetEntity_tags" ON "AssetEntity__AssetEntity_tags"."id" = "AssetEntity_AssetEntity__AssetEntity_tags"."tagsId"
LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id"
LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId"
LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id"
WHERE
("AssetEntity"."id" IN ($1))
@ -317,7 +323,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
"AssetEntity"."stackId" AS "AssetEntity_stackId"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
@ -362,7 +368,7 @@ FROM
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
"AssetEntity"."stackId" AS "AssetEntity_stackId"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
@ -426,7 +432,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
"AssetEntity"."stackId" AS "AssetEntity_stackId"
FROM
"assets" "AssetEntity"
WHERE
@ -472,7 +478,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
"AssetEntity"."stackId" AS "AssetEntity_stackId"
FROM
"assets" "AssetEntity"
WHERE
@ -516,7 +522,7 @@ SELECT
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
"AssetEntity"."stackId" AS "AssetEntity_stackId"
FROM
"assets" "AssetEntity"
WHERE
@ -550,8 +556,9 @@ SELECT
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "assets" "stack" ON "stack"."stackParentId" = "asset"."id"
AND ("stack"."deletedAt" IS NULL)
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
("asset"."isVisible" = true)
AND ("asset"."deletedAt" IS NULL)
@ -600,7 +607,7 @@ SELECT
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackParentId" AS "asset_stackParentId",
"asset"."stackId" AS "asset_stackId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@ -615,6 +622,7 @@ SELECT
"exifInfo"."projectionType" AS "exifInfo_projectionType",
"exifInfo"."city" AS "exifInfo_city",
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
"exifInfo"."state" AS "exifInfo_state",
"exifInfo"."country" AS "exifInfo_country",
"exifInfo"."make" AS "exifInfo_make",
@ -629,39 +637,42 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."deviceAssetId" AS "stack_deviceAssetId",
"stack"."ownerId" AS "stack_ownerId",
"stack"."libraryId" AS "stack_libraryId",
"stack"."deviceId" AS "stack_deviceId",
"stack"."type" AS "stack_type",
"stack"."originalPath" AS "stack_originalPath",
"stack"."resizePath" AS "stack_resizePath",
"stack"."webpPath" AS "stack_webpPath",
"stack"."thumbhash" AS "stack_thumbhash",
"stack"."encodedVideoPath" AS "stack_encodedVideoPath",
"stack"."createdAt" AS "stack_createdAt",
"stack"."updatedAt" AS "stack_updatedAt",
"stack"."deletedAt" AS "stack_deletedAt",
"stack"."fileCreatedAt" AS "stack_fileCreatedAt",
"stack"."localDateTime" AS "stack_localDateTime",
"stack"."fileModifiedAt" AS "stack_fileModifiedAt",
"stack"."isFavorite" AS "stack_isFavorite",
"stack"."isArchived" AS "stack_isArchived",
"stack"."isExternal" AS "stack_isExternal",
"stack"."isReadOnly" AS "stack_isReadOnly",
"stack"."isOffline" AS "stack_isOffline",
"stack"."checksum" AS "stack_checksum",
"stack"."duration" AS "stack_duration",
"stack"."isVisible" AS "stack_isVisible",
"stack"."livePhotoVideoId" AS "stack_livePhotoVideoId",
"stack"."originalFileName" AS "stack_originalFileName",
"stack"."sidecarPath" AS "stack_sidecarPath",
"stack"."stackParentId" AS "stack_stackParentId"
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "assets" "stack" ON "stack"."stackParentId" = "asset"."id"
AND ("stack"."deletedAt" IS NULL)
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
(
"asset"."isVisible" = true

View File

@ -172,7 +172,7 @@ FROM
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
"AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId"
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId"
FROM
"asset_faces" "AssetFaceEntity"
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
@ -270,7 +270,7 @@ FROM
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
"AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
"AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId",
@ -303,6 +303,7 @@ FROM
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
@ -376,7 +377,7 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
"AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId"
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId"
FROM
"asset_faces" "AssetFaceEntity"
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"

View File

@ -49,7 +49,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId",
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId",
"SharedLinkEntity__SharedLinkEntity_assets"."stackId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_assetId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."description" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_description",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."exifImageWidth" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exifImageWidth",
@ -64,6 +64,7 @@ FROM
"9b1d35b344d838023994a3233afd6ffe098be6d8"."projectionType" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_projectionType",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."city" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_city",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."livePhotoCID" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_livePhotoCID",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."autoStackId" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_autoStackId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."state" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_state",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."country" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_country",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."make" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_make",
@ -114,7 +115,7 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."livePhotoVideoId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_livePhotoVideoId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalFileName" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalFileName",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."sidecarPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_sidecarPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."stackParentId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_stackParentId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."stackId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_stackId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_assetId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."description" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_description",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifImageWidth" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exifImageWidth",
@ -129,6 +130,7 @@ FROM
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."projectionType" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_projectionType",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."city" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_city",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."livePhotoCID" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_livePhotoCID",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."autoStackId" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_autoStackId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."state" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_state",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."country" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_country",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."make" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_make",
@ -236,7 +238,7 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId",
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId",
"SharedLinkEntity__SharedLinkEntity_assets"."stackId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackId",
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
"SharedLinkEntity__SharedLinkEntity_album"."albumName" AS "SharedLinkEntity__SharedLinkEntity_album_albumName",

View File

@ -35,7 +35,7 @@ SELECT
"a"."livePhotoVideoId" AS "a_livePhotoVideoId",
"a"."originalFileName" AS "a_originalFileName",
"a"."sidecarPath" AS "a_sidecarPath",
"a"."stackParentId" AS "a_stackParentId",
"a"."stackId" AS "a_stackId",
"e"."assetId" AS "e_assetId",
"e"."description" AS "e_description",
"e"."exifImageWidth" AS "e_exifImageWidth",
@ -50,6 +50,7 @@ SELECT
"e"."projectionType" AS "e_projectionType",
"e"."city" AS "e_city",
"e"."livePhotoCID" AS "e_livePhotoCID",
"e"."autoStackId" AS "e_autoStackId",
"e"."state" AS "e_state",
"e"."country" AS "e_country",
"e"."make" AS "e_make",

View File

@ -1,9 +1,18 @@
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { AssetEntity, AssetStackEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { authStub } from './auth.stub';
import { fileStub } from './file.stub';
import { libraryStub } from './library.stub';
import { userStub } from './user.stub';
export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetStackEntity => {
return {
id: stackId,
assets: assets,
primaryAsset: assets[0],
primaryAssetId: assets[0].id,
};
};
export const assetStub = {
noResizePath: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -120,7 +129,7 @@ export const assetStub = {
}),
primaryImage: Object.freeze<AssetEntity>({
id: 'asset-id',
id: 'primary-asset-id',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
@ -157,7 +166,11 @@ export const assetStub = {
exifInfo: {
fileSizeInByte: 5_000,
} as ExifEntity,
stack: [{ id: 'stack-child-asset-1' } as AssetEntity, { id: 'stack-child-asset-2' } as AssetEntity],
stack: assetStackStub('stack-1', [
{ id: 'primary-asset-id' } as AssetEntity,
{ id: 'stack-child-asset-1' } as AssetEntity,
{ id: 'stack-child-asset-2' } as AssetEntity,
]),
}),
image: Object.freeze<AssetEntity>({

View File

@ -248,6 +248,7 @@ export const sharedLinkStub = {
profileDescription: 'sRGB',
bitsPerSample: 8,
colorspace: 'sRGB',
autoStackId: null,
},
tags: [],
sharedLinks: [],

View File

@ -0,0 +1,10 @@
import { IAssetStackRepository } from '@app/domain';
export const newAssetStackRepositoryMock = (): jest.Mocked<IAssetStackRepository> => {
return {
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
getById: jest.fn(),
};
};

View File

@ -1,6 +1,7 @@
export * from './access.repository.mock';
export * from './album.repository.mock';
export * from './api-key.repository.mock';
export * from './asset-stack.repository.mock';
export * from './asset.repository.mock';
export * from './audit.repository.mock';
export * from './communication.repository.mock';