You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-06 06:07:29 +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* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} |
|
||||||
*PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} |
|
*PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} |
|
||||||
*PeopleApi* | [**createPerson**](doc//PeopleApi.md#createperson) | **POST** /people |
|
*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* | [**getAllPeople**](doc//PeopleApi.md#getallpeople) | **GET** /people |
|
||||||
*PeopleApi* | [**getPerson**](doc//PeopleApi.md#getperson) | **GET** /people/{id} |
|
*PeopleApi* | [**getPerson**](doc//PeopleApi.md#getperson) | **GET** /people/{id} |
|
||||||
*PeopleApi* | [**getPersonStatistics**](doc//PeopleApi.md#getpersonstatistics) | **GET** /people/{id}/statistics |
|
*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;
|
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].
|
/// Performs an HTTP 'GET /people' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
@ -4546,6 +4546,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/people": {
|
"/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": {
|
"get": {
|
||||||
"operationId": "getAllPeople",
|
"operationId": "getAllPeople",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@ -4711,6 +4744,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/people/{id}": {
|
"/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": {
|
"get": {
|
||||||
"operationId": "getPerson",
|
"operationId": "getPerson",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
@ -2769,6 +2769,15 @@ export function updatePartner({ id, updatePartnerDto }: {
|
|||||||
body: 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 }: {
|
export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: {
|
||||||
closestAssetId?: string;
|
closestAssetId?: string;
|
||||||
closestPersonId?: string;
|
closestPersonId?: string;
|
||||||
@ -2813,6 +2822,14 @@ export function updatePeople({ peopleUpdateDto }: {
|
|||||||
body: 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 }: {
|
export function getPerson({ id }: {
|
||||||
id: string;
|
id: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, 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', () => {
|
describe('GET /people/:id', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`);
|
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', () => {
|
describe('POST /people/:id/merge', () => {
|
||||||
it('should be an authenticated route', async () => {
|
it('should be an authenticated route', async () => {
|
||||||
await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`);
|
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 { ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
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 { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceUpdateDto,
|
AssetFaceUpdateDto,
|
||||||
@ -49,6 +62,13 @@ export class PersonController {
|
|||||||
return this.service.updateAll(auth, dto);
|
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')
|
@Get(':id')
|
||||||
@Authenticated({ permission: Permission.PERSON_READ })
|
@Authenticated({ permission: Permission.PERSON_READ })
|
||||||
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
||||||
@ -65,6 +85,13 @@ export class PersonController {
|
|||||||
return this.service.update(auth, id, dto);
|
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')
|
@Get(':id/statistics')
|
||||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||||
|
@ -328,3 +328,12 @@ set
|
|||||||
"deletedAt" = $1
|
"deletedAt" = $1
|
||||||
where
|
where
|
||||||
"asset_faces"."id" = $2
|
"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 { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
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 { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
|
||||||
import { removeUndefinedKeys } from 'src/utils/database';
|
import { removeUndefinedKeys } from 'src/utils/database';
|
||||||
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
|
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
|
||||||
@ -102,6 +102,7 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
|
@Chunked()
|
||||||
async delete(ids: string[]): Promise<void> {
|
async delete(ids: string[]): Promise<void> {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -517,4 +518,13 @@ export class PersonRepository {
|
|||||||
await sql`REINDEX TABLE face_search`.execute(this.db);
|
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 { Person } from 'src/database';
|
||||||
import { AssetFaces, FaceSearch } from 'src/db';
|
import { AssetFaces, FaceSearch } from 'src/db';
|
||||||
import { Chunked, OnJob } from 'src/decorators';
|
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 { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceCreateDto,
|
AssetFaceCreateDto,
|
||||||
@ -216,6 +216,10 @@ export class PersonService extends BaseService {
|
|||||||
return mapPerson(person);
|
return mapPerson(person);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
|
return this.deleteAll(auth, { ids: [id] });
|
||||||
|
}
|
||||||
|
|
||||||
async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||||
const results: BulkIdResponseDto[] = [];
|
const results: BulkIdResponseDto[] = [];
|
||||||
for (const person of dto.people) {
|
for (const person of dto.people) {
|
||||||
@ -236,8 +240,14 @@ export class PersonService extends BaseService {
|
|||||||
return results;
|
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()
|
@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 Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath)));
|
||||||
await this.personRepository.delete(people.map((person) => person.id));
|
await this.personRepository.delete(people.map((person) => person.id));
|
||||||
this.logger.debug(`Deleted ${people.length} people`);
|
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 })
|
@OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||||
async handlePersonCleanup(): Promise<JobStatus> {
|
async handlePersonCleanup(): Promise<JobStatus> {
|
||||||
const people = await this.personRepository.getAllWithoutFaces();
|
const people = await this.personRepository.getAllWithoutFaces();
|
||||||
await this.delete(people);
|
await this.removeAllPeople(people);
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,7 +599,7 @@ export class PersonService extends BaseService {
|
|||||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||||
|
|
||||||
await this.personRepository.reassignFaces(mergeData);
|
await this.personRepository.reassignFaces(mergeData);
|
||||||
await this.delete([mergePerson]);
|
await this.removeAllPeople([mergePerson]);
|
||||||
|
|
||||||
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
|
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
|
||||||
results.push({ id: mergeId, success: true });
|
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);
|
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: {
|
case Permission.PERSON_CREATE: {
|
||||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
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: {
|
case Permission.PERSON_REASSIGN: {
|
||||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
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 { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
|
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.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 { PersonRepository } from 'src/repositories/person.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { SessionRepository } from 'src/repositories/session.repository';
|
import { SessionRepository } from 'src/repositories/session.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.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 Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
|
||||||
type RepositoriesTypes = {
|
type RepositoriesTypes = {
|
||||||
|
access: AccessRepository;
|
||||||
activity: ActivityRepository;
|
activity: ActivityRepository;
|
||||||
album: AlbumRepository;
|
album: AlbumRepository;
|
||||||
albumUser: AlbumUserRepository;
|
albumUser: AlbumUserRepository;
|
||||||
@ -58,6 +61,7 @@ type RepositoriesTypes = {
|
|||||||
person: PersonRepository;
|
person: PersonRepository;
|
||||||
search: SearchRepository;
|
search: SearchRepository;
|
||||||
session: SessionRepository;
|
session: SessionRepository;
|
||||||
|
storage: StorageRepository;
|
||||||
sync: SyncRepository;
|
sync: SyncRepository;
|
||||||
systemMetadata: SystemMetadataRepository;
|
systemMetadata: SystemMetadataRepository;
|
||||||
versionHistory: VersionHistoryRepository;
|
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>) => {
|
export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
case 'access': {
|
||||||
|
return new AccessRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
case 'activity': {
|
case 'activity': {
|
||||||
return new ActivityRepository(db);
|
return new ActivityRepository(db);
|
||||||
}
|
}
|
||||||
@ -352,6 +360,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
|||||||
return automock(SessionRepository);
|
return automock(SessionRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'storage': {
|
||||||
|
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
|
||||||
|
}
|
||||||
|
|
||||||
case 'sync': {
|
case 'sync': {
|
||||||
return automock(SyncRepository);
|
return automock(SyncRepository);
|
||||||
}
|
}
|
||||||
@ -411,7 +423,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
|||||||
repositories.session || getRepositoryMock('session'),
|
repositories.session || getRepositoryMock('session'),
|
||||||
repositories.sharedLink,
|
repositories.sharedLink,
|
||||||
repositories.stack,
|
repositories.stack,
|
||||||
repositories.storage,
|
repositories.storage || getRepositoryMock('storage'),
|
||||||
repositories.sync || getRepositoryMock('sync'),
|
repositories.sync || getRepositoryMock('sync'),
|
||||||
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
|
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
|
||||||
repositories.tag,
|
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 { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||||
import { newMetadataRepositoryMock } from 'test/repositories/metadata.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 { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||||
@ -278,7 +277,7 @@ export const newTestService = <T extends BaseService>(
|
|||||||
notification: automock(NotificationRepository),
|
notification: automock(NotificationRepository),
|
||||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||||
partner: automock(PartnerRepository, { strict: false }),
|
partner: automock(PartnerRepository, { strict: false }),
|
||||||
person: newPersonRepositoryMock(),
|
person: automock(PersonRepository, { strict: false }),
|
||||||
process: automock(ProcessRepository),
|
process: automock(ProcessRepository),
|
||||||
search: automock(SearchRepository, { strict: false }),
|
search: automock(SearchRepository, { strict: false }),
|
||||||
// eslint-disable-next-line no-sparse-arrays
|
// eslint-disable-next-line no-sparse-arrays
|
||||||
|
Reference in New Issue
Block a user