You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-03 05:46:58 +02:00
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -169,6 +169,8 @@ Class | Method | HTTP request | Description
|
||||
*PartnersApi* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} |
|
||||
*PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} |
|
||||
*PeopleApi* | [**createPerson**](doc//PeopleApi.md#createperson) | **POST** /people |
|
||||
*PeopleApi* | [**deletePeople**](doc//PeopleApi.md#deletepeople) | **DELETE** /people |
|
||||
*PeopleApi* | [**deletePerson**](doc//PeopleApi.md#deleteperson) | **DELETE** /people/{id} |
|
||||
*PeopleApi* | [**getAllPeople**](doc//PeopleApi.md#getallpeople) | **GET** /people |
|
||||
*PeopleApi* | [**getPerson**](doc//PeopleApi.md#getperson) | **GET** /people/{id} |
|
||||
*PeopleApi* | [**getPersonStatistics**](doc//PeopleApi.md#getpersonstatistics) | **GET** /people/{id}/statistics |
|
||||
|
79
mobile/openapi/lib/api/people_api.dart
generated
79
mobile/openapi/lib/api/people_api.dart
generated
@ -63,6 +63,85 @@ class PeopleApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /people' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||
Future<Response> deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/people';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = bulkIdsDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||
Future<void> deletePeople(BulkIdsDto bulkIdsDto,) async {
|
||||
final response = await deletePeopleWithHttpInfo(bulkIdsDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /people/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deletePersonWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/people/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deletePerson(String id,) async {
|
||||
final response = await deletePersonWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /people' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
@ -4546,6 +4546,39 @@
|
||||
}
|
||||
},
|
||||
"/people": {
|
||||
"delete": {
|
||||
"operationId": "deletePeople",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BulkIdsDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"People"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getAllPeople",
|
||||
"parameters": [
|
||||
@ -4711,6 +4744,39 @@
|
||||
}
|
||||
},
|
||||
"/people/{id}": {
|
||||
"delete": {
|
||||
"operationId": "deletePerson",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"People"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getPerson",
|
||||
"parameters": [
|
||||
|
@ -2769,6 +2769,15 @@ export function updatePartner({ id, updatePartnerDto }: {
|
||||
body: updatePartnerDto
|
||||
})));
|
||||
}
|
||||
export function deletePeople({ bulkIdsDto }: {
|
||||
bulkIdsDto: BulkIdsDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/people", oazapfts.json({
|
||||
...opts,
|
||||
method: "DELETE",
|
||||
body: bulkIdsDto
|
||||
})));
|
||||
}
|
||||
export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: {
|
||||
closestAssetId?: string;
|
||||
closestPersonId?: string;
|
||||
@ -2813,6 +2822,14 @@ export function updatePeople({ peopleUpdateDto }: {
|
||||
body: peopleUpdateDto
|
||||
})));
|
||||
}
|
||||
export function deletePerson({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/people/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getPerson({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
@ -60,6 +60,29 @@ describe(PersonController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /people', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete('/people');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require uuids in the body', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.delete('/people')
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.delete(`/people`)
|
||||
.send({ ids: [factory.uuid()] });
|
||||
expect(status).toBe(204);
|
||||
expect(service.deleteAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /people/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`);
|
||||
@ -156,6 +179,25 @@ describe(PersonController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /people/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
const { status } = await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`);
|
||||
expect(status).toBe(204);
|
||||
expect(service.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /people/:id/merge', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`);
|
||||
|
@ -1,7 +1,20 @@
|
||||
import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Next,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
AssetFaceUpdateDto,
|
||||
@ -49,6 +62,13 @@ export class PersonController {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PERSON_DELETE })
|
||||
deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.PERSON_READ })
|
||||
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
||||
@ -65,6 +85,13 @@ export class PersonController {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PERSON_DELETE })
|
||||
deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||
|
@ -328,3 +328,12 @@ set
|
||||
"deletedAt" = $1
|
||||
where
|
||||
"asset_faces"."id" = $2
|
||||
|
||||
-- PersonRepository.getForPeopleDelete
|
||||
select
|
||||
"id",
|
||||
"thumbnailPath"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"id" in ($1)
|
||||
|
@ -3,7 +3,7 @@ import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } fr
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
|
||||
import { removeUndefinedKeys } from 'src/utils/database';
|
||||
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
|
||||
@ -102,6 +102,7 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@Chunked()
|
||||
async delete(ids: string[]): Promise<void> {
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
@ -517,4 +518,13 @@ export class PersonRepository {
|
||||
await sql`REINDEX TABLE face_search`.execute(this.db);
|
||||
}
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@Chunked()
|
||||
getForPeopleDelete(ids: string[]) {
|
||||
if (ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.db.selectFrom('person').select(['id', 'thumbnailPath']).where('id', 'in', ids).execute();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { Person } from 'src/database';
|
||||
import { AssetFaces, FaceSearch } from 'src/db';
|
||||
import { Chunked, OnJob } from 'src/decorators';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
AssetFaceCreateDto,
|
||||
@ -216,6 +216,10 @@ export class PersonService extends BaseService {
|
||||
return mapPerson(person);
|
||||
}
|
||||
|
||||
delete(auth: AuthDto, id: string): Promise<void> {
|
||||
return this.deleteAll(auth, { ids: [id] });
|
||||
}
|
||||
|
||||
async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const person of dto.people) {
|
||||
@ -236,8 +240,14 @@ export class PersonService extends BaseService {
|
||||
return results;
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, { ids }: BulkIdsDto): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_DELETE, ids });
|
||||
const people = await this.personRepository.getForPeopleDelete(ids);
|
||||
await this.removeAllPeople(people);
|
||||
}
|
||||
|
||||
@Chunked()
|
||||
private async delete(people: { id: string; thumbnailPath: string }[]) {
|
||||
private async removeAllPeople(people: { id: string; thumbnailPath: string }[]) {
|
||||
await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath)));
|
||||
await this.personRepository.delete(people.map((person) => person.id));
|
||||
this.logger.debug(`Deleted ${people.length} people`);
|
||||
@ -246,7 +256,7 @@ export class PersonService extends BaseService {
|
||||
@OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||
async handlePersonCleanup(): Promise<JobStatus> {
|
||||
const people = await this.personRepository.getAllWithoutFaces();
|
||||
await this.delete(people);
|
||||
await this.removeAllPeople(people);
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@ -589,7 +599,7 @@ export class PersonService extends BaseService {
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
||||
await this.personRepository.reassignFaces(mergeData);
|
||||
await this.delete([mergePerson]);
|
||||
await this.removeAllPeople([mergePerson]);
|
||||
|
||||
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
|
||||
results.push({ id: mergeId, success: true });
|
||||
|
@ -256,22 +256,17 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_UPDATE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_CREATE: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ:
|
||||
case Permission.PERSON_UPDATE:
|
||||
case Permission.PERSON_DELETE:
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_REASSIGN: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { AssetFace } from 'src/database';
|
||||
import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
@ -24,6 +25,7 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
@ -40,6 +42,7 @@ const sha256 = (value: string) => createHash('sha256').update(value).digest('bas
|
||||
|
||||
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
|
||||
type RepositoriesTypes = {
|
||||
access: AccessRepository;
|
||||
activity: ActivityRepository;
|
||||
album: AlbumRepository;
|
||||
albumUser: AlbumUserRepository;
|
||||
@ -58,6 +61,7 @@ type RepositoriesTypes = {
|
||||
person: PersonRepository;
|
||||
search: SearchRepository;
|
||||
session: SessionRepository;
|
||||
storage: StorageRepository;
|
||||
sync: SyncRepository;
|
||||
systemMetadata: SystemMetadataRepository;
|
||||
versionHistory: VersionHistoryRepository;
|
||||
@ -180,6 +184,10 @@ export const newMediumService = <R extends RepositoryOptions, S extends BaseServ
|
||||
|
||||
export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
|
||||
switch (key) {
|
||||
case 'access': {
|
||||
return new AccessRepository(db);
|
||||
}
|
||||
|
||||
case 'activity': {
|
||||
return new ActivityRepository(db);
|
||||
}
|
||||
@ -352,6 +360,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
return automock(SessionRepository);
|
||||
}
|
||||
|
||||
case 'storage': {
|
||||
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
|
||||
}
|
||||
|
||||
case 'sync': {
|
||||
return automock(SyncRepository);
|
||||
}
|
||||
@ -411,7 +423,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
||||
repositories.session || getRepositoryMock('session'),
|
||||
repositories.sharedLink,
|
||||
repositories.stack,
|
||||
repositories.storage,
|
||||
repositories.storage || getRepositoryMock('storage'),
|
||||
repositories.sync || getRepositoryMock('sync'),
|
||||
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
|
||||
repositories.tag,
|
||||
|
90
server/test/medium/specs/services/person.service.spec.ts
Normal file
90
server/test/medium/specs/services/person.service.spec.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
describe.concurrent(PersonService.name, () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const createSut = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PersonService, {
|
||||
database: db || defaultDatabase,
|
||||
repos: {
|
||||
access: 'real',
|
||||
database: 'real',
|
||||
person: 'real',
|
||||
storage: 'mock',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw an error when there is no access', async () => {
|
||||
const { sut } = createSut();
|
||||
const auth = factory.auth();
|
||||
const personId = factory.uuid();
|
||||
await expect(sut.delete(auth, personId)).rejects.toThrow('Not found or no person.delete access');
|
||||
});
|
||||
|
||||
it('should delete the person', async () => {
|
||||
const { sut, getRepository, mocks } = createSut();
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
const auth = factory.auth({ user });
|
||||
const person = mediumFactory.personInsert({ ownerId: auth.user.id });
|
||||
mocks.storage.unlink.mockResolvedValue();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
await userRepo.create(user);
|
||||
|
||||
const personRepo = getRepository('person');
|
||||
await personRepo.create(person);
|
||||
|
||||
await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id }));
|
||||
await expect(sut.delete(auth, person.id)).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person.id)).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
it('should throw an error when there is no access', async () => {
|
||||
const { sut } = createSut();
|
||||
const auth = factory.auth();
|
||||
const personId = factory.uuid();
|
||||
await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access');
|
||||
});
|
||||
|
||||
it('should delete the person', async () => {
|
||||
const { sut, getRepository, mocks } = createSut();
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
const auth = factory.auth({ user });
|
||||
const person1 = mediumFactory.personInsert({ ownerId: auth.user.id });
|
||||
const person2 = mediumFactory.personInsert({ ownerId: auth.user.id });
|
||||
mocks.storage.unlink.mockResolvedValue();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
await userRepo.create(user);
|
||||
|
||||
const personRepo = getRepository('person');
|
||||
await personRepo.create(person1);
|
||||
await personRepo.create(person2);
|
||||
|
||||
await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person1.id)).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person2.id)).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person1.thumbnailPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person2.thumbnailPath);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepository>> => {
|
||||
return {
|
||||
reassignFaces: vitest.fn(),
|
||||
unassignFaces: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
deleteFaces: vitest.fn(),
|
||||
getAllFaces: vitest.fn(),
|
||||
getAll: vitest.fn(),
|
||||
getAllForUser: vitest.fn(),
|
||||
getAllWithoutFaces: vitest.fn(),
|
||||
getFaces: vitest.fn(),
|
||||
getFaceById: vitest.fn(),
|
||||
getFaceForFacialRecognitionJob: vitest.fn(),
|
||||
getDataForThumbnailGenerationJob: vitest.fn(),
|
||||
reassignFace: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
getByName: vitest.fn(),
|
||||
getDistinctNames: vitest.fn(),
|
||||
getStatistics: vitest.fn(),
|
||||
getNumberOfPeople: vitest.fn(),
|
||||
create: vitest.fn(),
|
||||
createAll: vitest.fn(),
|
||||
refreshFaces: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
updateAll: vitest.fn(),
|
||||
getFacesByIds: vitest.fn(),
|
||||
getRandomFace: vitest.fn(),
|
||||
getLatestFaceDate: vitest.fn(),
|
||||
createAssetFace: vitest.fn(),
|
||||
deleteAssetFace: vitest.fn(),
|
||||
softDeleteAssetFaces: vitest.fn(),
|
||||
vacuum: vitest.fn(),
|
||||
};
|
||||
};
|
@ -67,7 +67,6 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
@ -278,7 +277,7 @@ export const newTestService = <T extends BaseService>(
|
||||
notification: automock(NotificationRepository),
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: newPersonRepositoryMock(),
|
||||
person: automock(PersonRepository, { strict: false }),
|
||||
process: automock(ProcessRepository),
|
||||
search: automock(SearchRepository, { strict: false }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
|
Reference in New Issue
Block a user