mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server, web): smart merge (#5796)
* pr feedback * fix: tests * update assets statistics * pr feedback * pr feedback * fix: linter * pr feedback * fix: don't limit the smart merge * pr feedback * fix: server code --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
c55503496f
commit
f0b328fb6b
8 changed files with 168 additions and 91 deletions
|
|
@ -834,7 +834,7 @@ describe(PersonService.name, () => {
|
|||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should merge two people', async () => {
|
||||
it('should merge two people without smart merge', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.delete.mockResolvedValue(personStub.mergePerson);
|
||||
|
|
@ -850,6 +850,8 @@ describe(PersonService.name, () => {
|
|||
oldPersonId: personStub.mergePerson.id,
|
||||
});
|
||||
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.PERSON_DELETE,
|
||||
data: { id: personStub.mergePerson.id },
|
||||
|
|
@ -857,6 +859,35 @@ describe(PersonService.name, () => {
|
|||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should merge two people with smart merge', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.randomPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.delete.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name });
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-3', { ids: ['person-1'] })).resolves.toEqual([
|
||||
{ id: 'person-1', success: true },
|
||||
]);
|
||||
|
||||
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.randomPerson.id,
|
||||
oldPersonId: personStub.primaryPerson.id,
|
||||
});
|
||||
|
||||
expect(personMock.update).toHaveBeenCalledWith({
|
||||
id: personStub.randomPerson.id,
|
||||
name: personStub.primaryPerson.name,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.PERSON_DELETE,
|
||||
data: { id: personStub.primaryPerson.id },
|
||||
});
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should throw an error when the primary person is not found', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
|
@ -885,8 +916,8 @@ describe(PersonService.name, () => {
|
|||
});
|
||||
|
||||
it('should handle an error reassigning faces', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValue(personStub.mergePerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.reassignFaces.mockRejectedValue(new Error('update failed'));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ export class PersonService {
|
|||
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
||||
const mergeIds = dto.ids;
|
||||
await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
|
||||
const primaryPerson = await this.findOrFail(id);
|
||||
let primaryPerson = await this.findOrFail(id);
|
||||
const primaryName = primaryPerson.name || primaryPerson.id;
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
|
|
@ -481,6 +481,19 @@ export class PersonService {
|
|||
continue;
|
||||
}
|
||||
|
||||
const update: Partial<PersonEntity> = {};
|
||||
if (!primaryPerson.name && mergePerson.name) {
|
||||
update.name = mergePerson.name;
|
||||
}
|
||||
|
||||
if (!primaryPerson.birthDate && mergePerson.birthDate) {
|
||||
update.birthDate = mergePerson.birthDate;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update });
|
||||
}
|
||||
|
||||
const mergeName = mergePerson.name || mergePerson.id;
|
||||
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
|
@ -495,7 +508,6 @@ export class PersonService {
|
|||
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
|
|||
14
server/test/fixtures/person.stub.ts
vendored
14
server/test/fixtures/person.stub.ts
vendored
|
|
@ -128,4 +128,18 @@ export const personStub = {
|
|||
faceAsset: null,
|
||||
isHidden: false,
|
||||
}),
|
||||
randomPerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-3',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
ownerId: userStub.admin.id,
|
||||
owner: userStub.admin,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue