mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
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:
parent
c4b8c853bc
commit
25cad79657
29 changed files with 538 additions and 243 deletions
|
@ -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 () => {
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -156,5 +156,8 @@
|
|||
"^@app/domain(|/.*)$": "<rootDir>/src/domain/$1"
|
||||
},
|
||||
"globalSetup": "<rootDir>/test/global-setup.js"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
|
||||
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(assetStackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' });
|
||||
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], {
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,7 +301,9 @@ export class AssetService {
|
|||
person: true,
|
||||
},
|
||||
stack: {
|
||||
exifInfo: true,
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
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;
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
// 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)));
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
10
server/src/domain/repositories/asset-stack.repository.ts
Normal file
10
server/src/domain/repositories/asset-stack.repository.ts
Normal 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>;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
19
server/src/infra/entities/asset-stack.entity.ts
Normal file
19
server/src/infra/entities/asset-stack.entity.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
16
server/src/infra/migrations/1703035138085-AddAutoStackId.ts
Normal file
16
server/src/infra/migrations/1703035138085-AddAutoStackId.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -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"`);
|
||||
}
|
||||
}
|
47
server/src/infra/repositories/asset-stack.repository.ts
Normal file
47
server/src/infra/repositories/asset-stack.repository.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
builder.where(where);
|
||||
|
||||
if (withStacked) {
|
||||
builder
|
||||
.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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
19
server/test/fixtures/asset.stub.ts
vendored
19
server/test/fixtures/asset.stub.ts
vendored
|
@ -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>({
|
||||
|
|
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -248,6 +248,7 @@ export const sharedLinkStub = {
|
|||
profileDescription: 'sRGB',
|
||||
bitsPerSample: 8,
|
||||
colorspace: 'sRGB',
|
||||
autoStackId: null,
|
||||
},
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
|
|
10
server/test/repositories/asset-stack.repository.mock.ts
Normal file
10
server/test/repositories/asset-stack.repository.mock.ts
Normal 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(),
|
||||
};
|
||||
};
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue