mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: persistent memories (#8330)
* feat: persistent memories * refactor: use new add/remove asset utility
This commit is contained in:
parent
0849dbd1af
commit
cd0e537e3e
43 changed files with 3497 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ import { DownloadController } from 'src/controllers/download.controller';
|
|||
import { FaceController } from 'src/controllers/face.controller';
|
||||
import { JobController } from 'src/controllers/job.controller';
|
||||
import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { MemoryController } from 'src/controllers/memory.controller';
|
||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||
import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { PersonController } from 'src/controllers/person.controller';
|
||||
|
|
@ -36,6 +37,7 @@ export const controllers = [
|
|||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MemoryController,
|
||||
OAuthController,
|
||||
PartnerController,
|
||||
SearchController,
|
||||
|
|
|
|||
64
server/src/controllers/memory.controller.ts
Normal file
64
server/src/controllers/memory.controller.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Memory')
|
||||
@Controller('memories')
|
||||
@Authenticated()
|
||||
export class MemoryController {
|
||||
constructor(private service: MemoryService) {}
|
||||
|
||||
@Get()
|
||||
searchMemories(@Auth() auth: AuthDto): Promise<MemoryResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Post()
|
||||
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
updateMemory(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: MemoryUpdateDto,
|
||||
): Promise<MemoryResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.remove(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/assets')
|
||||
addMemoryAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: BulkIdsDto,
|
||||
): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.addAssets(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/assets')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
removeMemoryAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: BulkIdsDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.removeAssets(auth, id, dto);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,10 @@ export enum Permission {
|
|||
TIMELINE_READ = 'timeline.read',
|
||||
TIMELINE_DOWNLOAD = 'timeline.download',
|
||||
|
||||
MEMORY_READ = 'memory.read',
|
||||
MEMORY_WRITE = 'memory.write',
|
||||
MEMORY_DELETE = 'memory.delete',
|
||||
|
||||
PERSON_READ = 'person.read',
|
||||
PERSON_WRITE = 'person.write',
|
||||
PERSON_MERGE = 'person.merge',
|
||||
|
|
@ -259,6 +263,18 @@ export class AccessCore {
|
|||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.MEMORY_READ: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_WRITE: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.MEMORY_DELETE: {
|
||||
return this.repository.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
|
|
|||
84
server/src/dtos/memory.dto.ts
Normal file
84
server/src/dtos/memory.dto.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { MemoryEntity, MemoryType } from 'src/entities/memory.entity';
|
||||
import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
class MemoryBaseDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isSaved?: boolean;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
seenAt?: Date;
|
||||
}
|
||||
|
||||
class OnThisDayDto {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
year!: number;
|
||||
}
|
||||
|
||||
type MemoryData = OnThisDayDto;
|
||||
|
||||
export class MemoryUpdateDto extends MemoryBaseDto {
|
||||
@ValidateDate({ optional: true })
|
||||
memoryAt?: Date;
|
||||
}
|
||||
|
||||
export class MemoryCreateDto extends MemoryBaseDto {
|
||||
@IsEnum(MemoryType)
|
||||
@ApiProperty({ enum: MemoryType, enumName: 'MemoryType' })
|
||||
type!: MemoryType;
|
||||
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type((options) => {
|
||||
switch (options?.object.type) {
|
||||
case MemoryType.ON_THIS_DAY: {
|
||||
return OnThisDayDto;
|
||||
}
|
||||
|
||||
default: {
|
||||
return Object;
|
||||
}
|
||||
}
|
||||
})
|
||||
data!: MemoryData;
|
||||
|
||||
@ValidateDate()
|
||||
memoryAt!: Date;
|
||||
|
||||
@ValidateUUID({ optional: true, each: true })
|
||||
assetIds?: string[];
|
||||
}
|
||||
|
||||
export class MemoryResponseDto {
|
||||
id!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
deletedAt?: Date;
|
||||
memoryAt!: Date;
|
||||
seenAt?: Date;
|
||||
ownerId!: string;
|
||||
type!: MemoryType;
|
||||
data!: MemoryData;
|
||||
isSaved!: boolean;
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export const mapMemory = (entity: MemoryEntity): MemoryResponseDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
deletedAt: entity.deletedAt,
|
||||
memoryAt: entity.memoryAt,
|
||||
seenAt: entity.seenAt,
|
||||
ownerId: entity.ownerId,
|
||||
type: entity.type,
|
||||
data: entity.data,
|
||||
isSaved: entity.isSaved,
|
||||
assets: entity.assets.map((asset) => mapAsset(asset)),
|
||||
};
|
||||
};
|
||||
|
|
@ -9,6 +9,7 @@ import { AuditEntity } from 'src/entities/audit.entity';
|
|||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
|
|
@ -32,6 +33,7 @@ export const entities = [
|
|||
AuditEntity,
|
||||
ExifEntity,
|
||||
GeodataPlacesEntity,
|
||||
MemoryEntity,
|
||||
MoveEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
|
|
|
|||
67
server/src/entities/memory.entity.ts
Normal file
67
server/src/entities/memory.entity.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum MemoryType {
|
||||
/** pictures taken on this day X years ago */
|
||||
ON_THIS_DAY = 'on_this_day',
|
||||
}
|
||||
|
||||
export type OnThisDayData = { year: number };
|
||||
|
||||
export interface MemoryData {
|
||||
[MemoryType.ON_THIS_DAY]: OnThisDayData;
|
||||
}
|
||||
|
||||
@Entity('memories')
|
||||
export class MemoryEntity<T extends MemoryType = MemoryType> {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
type!: T;
|
||||
|
||||
@Column({ type: 'jsonb' })
|
||||
data!: MemoryData[T];
|
||||
|
||||
/** unless set to true, will be automatically deleted in the future */
|
||||
@Column({ default: false })
|
||||
isSaved!: boolean;
|
||||
|
||||
/** memories are sorted in ascending order by this value */
|
||||
@Column({ type: 'timestamptz' })
|
||||
memoryAt!: Date;
|
||||
|
||||
/** when the user last viewed the memory */
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
seenAt?: Date;
|
||||
|
||||
@ManyToMany(() => AssetEntity)
|
||||
@JoinTable()
|
||||
assets!: AssetEntity[];
|
||||
}
|
||||
|
|
@ -32,6 +32,10 @@ export interface IAccessRepository {
|
|||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
memory: {
|
||||
checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
person: {
|
||||
checkFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
|
||||
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
|
||||
|
|
|
|||
14
server/src/interfaces/memory.interface.ts
Normal file
14
server/src/interfaces/memory.interface.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
|
||||
export const IMemoryRepository = 'IMemoryRepository';
|
||||
|
||||
export interface IMemoryRepository {
|
||||
search(ownerId: string): Promise<MemoryEntity[]>;
|
||||
get(id: string): Promise<MemoryEntity | null>;
|
||||
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
|
||||
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getAssetIds(id: string, assetIds: string[]): Promise<Set<string>>;
|
||||
addAssetIds(id: string, assetIds: string[]): Promise<void>;
|
||||
removeAssetIds(id: string, assetIds: string[]): Promise<void>;
|
||||
}
|
||||
26
server/src/migrations/1711637874206-AddMemoryTable.ts
Normal file
26
server/src/migrations/1711637874206-AddMemoryTable.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddMemoryTable1711637874206 implements MigrationInterface {
|
||||
name = 'AddMemoryTable1711637874206'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seenAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL, CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6942ecf52d75d4273de19d2c16" ON "memories_assets_assets" ("assetsId") `);
|
||||
await queryRunner.query(`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`);
|
||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`);
|
||||
await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`);
|
||||
await queryRunner.query(`DROP TABLE "memories_assets_assets"`);
|
||||
await queryRunner.query(`DROP TABLE "memories"`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -196,6 +196,20 @@ WHERE
|
|||
)
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- AccessRepository.memory.checkOwnerAccess
|
||||
SELECT
|
||||
"MemoryEntity"."id" AS "MemoryEntity_id"
|
||||
FROM
|
||||
"memories" "MemoryEntity"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("MemoryEntity"."id" IN ($1))
|
||||
AND ("MemoryEntity"."ownerId" = $2)
|
||||
)
|
||||
)
|
||||
AND ("MemoryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- AccessRepository.person.checkOwnerAccess
|
||||
SELECT
|
||||
"PersonEntity"."id" AS "PersonEntity_id"
|
||||
|
|
|
|||
18
server/src/queries/memory.repository.sql
Normal file
18
server/src/queries/memory.repository.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- MemoryRepository.getAssetIds
|
||||
SELECT
|
||||
"memories_assets"."assetsId" AS "assetId"
|
||||
FROM
|
||||
"memories_assets_assets" "memories_assets"
|
||||
WHERE
|
||||
"memories_assets"."memoriesId" = $1
|
||||
AND "memories_assets"."assetsId" IN ($2)
|
||||
|
||||
-- MemoryRepository.removeAssetIds
|
||||
DELETE FROM "memories_assets_assets"
|
||||
WHERE
|
||||
(
|
||||
"memoriesId" = $1
|
||||
AND "assetsId" IN ($2)
|
||||
)
|
||||
|
|
@ -5,6 +5,7 @@ import { AlbumEntity } from 'src/entities/album.entity';
|
|||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
|
|
@ -19,6 +20,7 @@ type IAssetAccess = IAccessRepository['asset'];
|
|||
type IAuthDeviceAccess = IAccessRepository['authDevice'];
|
||||
type ILibraryAccess = IAccessRepository['library'];
|
||||
type ITimelineAccess = IAccessRepository['timeline'];
|
||||
type IMemoryAccess = IAccessRepository['memory'];
|
||||
type IPersonAccess = IAccessRepository['person'];
|
||||
type IPartnerAccess = IAccessRepository['partner'];
|
||||
|
||||
|
|
@ -345,6 +347,28 @@ class TimelineAccess implements ITimelineAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class MemoryAccess implements IMemoryAccess {
|
||||
constructor(private memoryRepository: Repository<MemoryEntity>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>> {
|
||||
if (memoryIds.size === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return this.memoryRepository
|
||||
.find({
|
||||
select: { id: true },
|
||||
where: {
|
||||
id: In([...memoryIds]),
|
||||
ownerId: userId,
|
||||
},
|
||||
})
|
||||
.then((memories) => new Set(memories.map((memory) => memory.id)));
|
||||
}
|
||||
}
|
||||
|
||||
class PersonAccess implements IPersonAccess {
|
||||
constructor(
|
||||
private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
|
|
@ -416,6 +440,7 @@ export class AccessRepository implements IAccessRepository {
|
|||
asset: IAssetAccess;
|
||||
authDevice: IAuthDeviceAccess;
|
||||
library: ILibraryAccess;
|
||||
memory: IMemoryAccess;
|
||||
person: IPersonAccess;
|
||||
partner: IPartnerAccess;
|
||||
timeline: ITimelineAccess;
|
||||
|
|
@ -425,6 +450,7 @@ export class AccessRepository implements IAccessRepository {
|
|||
@InjectRepository(AssetEntity) assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) albumRepository: Repository<AlbumEntity>,
|
||||
@InjectRepository(LibraryEntity) libraryRepository: Repository<LibraryEntity>,
|
||||
@InjectRepository(MemoryEntity) memoryRepository: Repository<MemoryEntity>,
|
||||
@InjectRepository(PartnerEntity) partnerRepository: Repository<PartnerEntity>,
|
||||
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
|
|
@ -436,6 +462,7 @@ export class AccessRepository implements IAccessRepository {
|
|||
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
||||
this.authDevice = new AuthDeviceAccess(tokenRepository);
|
||||
this.library = new LibraryAccess(libraryRepository);
|
||||
this.memory = new MemoryAccess(memoryRepository);
|
||||
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
||||
this.partner = new PartnerAccess(partnerRepository);
|
||||
this.timeline = new TimelineAccess(partnerRepository);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { IJobRepository } from 'src/interfaces/job.interface';
|
|||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
|
|
@ -42,6 +43,7 @@ import { JobRepository } from 'src/repositories/job.repository';
|
|||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MetricRepository } from 'src/repositories/metric.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
|
|
@ -72,6 +74,7 @@ export const repositories = [
|
|||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||
{ provide: IMemoryRepository, useClass: MemoryRepository },
|
||||
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
||||
{ provide: IMetricRepository, useClass: MetricRepository },
|
||||
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||
|
|
|
|||
104
server/src/repositories/memory.repository.ts
Normal file
104
server/src/repositories/memory.repository.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class MemoryRepository implements IMemoryRepository {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(MemoryEntity) private repository: Repository<MemoryEntity>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
search(ownerId: string): Promise<MemoryEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
},
|
||||
order: {
|
||||
memoryAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get(id: string): Promise<MemoryEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity> {
|
||||
return this.save(memory);
|
||||
}
|
||||
|
||||
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity> {
|
||||
return this.save(memory);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete({ id });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async getAssetIds(id: string, assetIds: string[]): Promise<Set<string>> {
|
||||
if (assetIds.length === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const results = await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.select('memories_assets.assetsId', 'assetId')
|
||||
.from('memories_assets_assets', 'memories_assets')
|
||||
.where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id })
|
||||
.andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds })
|
||||
.getRawMany();
|
||||
|
||||
return new Set(results.map((row) => row['assetId']));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] })
|
||||
async addAssetIds(id: string, assetIds: string[]): Promise<void> {
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('memories_assets_assets', ['memoriesId', 'assetsId'])
|
||||
.values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
async removeAssetIds(id: string, assetIds: string[]): Promise<void> {
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('memories_assets_assets')
|
||||
.where({
|
||||
memoriesId: id,
|
||||
assetsId: In(assetIds),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private async save(memory: Partial<MemoryEntity>): Promise<MemoryEntity> {
|
||||
const { id } = await this.repository.save(memory);
|
||||
return this.repository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { DownloadService } from 'src/services/download.service';
|
|||
import { JobService } from 'src/services/job.service';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { MicroservicesService } from 'src/services/microservices.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
|
|
@ -42,6 +43,7 @@ export const services = [
|
|||
JobService,
|
||||
LibraryService,
|
||||
MediaService,
|
||||
MemoryService,
|
||||
MetadataService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
|
|
|
|||
214
server/src/services/memory.service.spec.ts
Normal file
214
server/src/services/memory.service.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { MemoryType } from 'src/entities/memory.entity';
|
||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { memoryStub } from 'test/fixtures/memory.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock';
|
||||
|
||||
describe(MemoryService.name, () => {
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let memoryMock: jest.Mocked<IMemoryRepository>;
|
||||
let sut: MemoryService;
|
||||
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
memoryMock = newMemoryRepositoryMock();
|
||||
|
||||
sut = new MemoryService(accessMock, memoryMock);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should search memories', async () => {
|
||||
memoryMock.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]);
|
||||
await expect(sut.search(authStub.admin)).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'memory1', assets: expect.any(Array) }),
|
||||
expect.objectContaining({ id: 'memoryEmpty', assets: [] }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should map ', async () => {
|
||||
await expect(sut.search(authStub.admin)).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should throw an error when no access', async () => {
|
||||
await expect(sut.get(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw an error when the memory is not found', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['race-condition']));
|
||||
await expect(sut.get(authStub.admin, 'race-condition')).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should get a memory by id', async () => {
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.get(authStub.admin, 'memory1')).resolves.toMatchObject({ id: 'memory1' });
|
||||
expect(memoryMock.get).toHaveBeenCalledWith('memory1');
|
||||
expect(accessMock.memory.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['memory1']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should skip assets the user does not have access to', async () => {
|
||||
memoryMock.create.mockResolvedValue(memoryStub.empty);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 },
|
||||
assetIds: ['not-mine'],
|
||||
memoryAt: new Date(2024),
|
||||
}),
|
||||
).resolves.toMatchObject({ assets: [] });
|
||||
expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] }));
|
||||
});
|
||||
|
||||
it('should create a memory', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.create.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 },
|
||||
assetIds: ['asset1'],
|
||||
memoryAt: new Date(2024),
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
expect(memoryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ownerId: userStub.admin.id,
|
||||
assets: [{ id: 'asset1' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a memory without assets', async () => {
|
||||
memoryMock.create.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 },
|
||||
memoryAt: new Date(2024),
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should require access', async () => {
|
||||
await expect(sut.update(authStub.admin, 'not-found', { isSaved: true })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.update.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined();
|
||||
expect(memoryMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'memory1',
|
||||
isSaved: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should require access', async () => {
|
||||
await expect(sut.remove(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(memoryMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete a memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.remove(authStub.admin, 'memory1')).resolves.toBeUndefined();
|
||||
expect(memoryMock.delete).toHaveBeenCalledWith('memory1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAssets', () => {
|
||||
it('should require memory access', async () => {
|
||||
await expect(sut.addAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require asset access', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([
|
||||
{ error: 'no_permission', id: 'not-found', success: false },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets already in the memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ error: 'duplicate', id: 'asset1', success: false },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add assets', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ id: 'asset1', success: true },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAssets', () => {
|
||||
it('should require memory access', async () => {
|
||||
await expect(sut.removeAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.removeAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets not in the memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([
|
||||
{ error: 'not_found', id: 'not-found', success: false },
|
||||
]);
|
||||
expect(memoryMock.removeAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require asset access', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ error: 'no_permission', id: 'asset1', success: false },
|
||||
]);
|
||||
expect(memoryMock.removeAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove assets', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ id: 'asset1', success: true },
|
||||
]);
|
||||
expect(memoryMock.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
105
server/src/services/memory.service.ts
Normal file
105
server/src/services/memory.service.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||
@Inject(IMemoryRepository) private repository: IMemoryRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
|
||||
async search(auth: AuthDto) {
|
||||
const memories = await this.repository.search(auth.user.id);
|
||||
return memories.map((memory) => mapMemory(memory));
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
|
||||
const memory = await this.findOrFail(id);
|
||||
return mapMemory(memory);
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: MemoryCreateDto) {
|
||||
// TODO validate type/data combination
|
||||
|
||||
const assetIds = dto.assetIds || [];
|
||||
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds);
|
||||
const memory = await this.repository.create({
|
||||
ownerId: auth.user.id,
|
||||
type: dto.type,
|
||||
data: dto.data,
|
||||
isSaved: dto.isSaved,
|
||||
memoryAt: dto.memoryAt,
|
||||
seenAt: dto.seenAt,
|
||||
assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
|
||||
return mapMemory(memory);
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
|
||||
|
||||
const memory = await this.repository.update({
|
||||
id,
|
||||
isSaved: dto.isSaved,
|
||||
memoryAt: dto.memoryAt,
|
||||
seenAt: dto.seenAt,
|
||||
});
|
||||
|
||||
return mapMemory(memory);
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id);
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_READ, id);
|
||||
|
||||
const repos = { accessRepository: this.accessRepository, repository: this.repository };
|
||||
const results = await addAssets(auth, repos, { id, assetIds: dto.ids });
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.repository.update({ id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id);
|
||||
|
||||
const repos = { accessRepository: this.accessRepository, repository: this.repository };
|
||||
const permissions = [Permission.ASSET_SHARE];
|
||||
const results = await removeAssets(auth, repos, { id, assetIds: dto.ids, permissions });
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.repository.update({ id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async findOrFail(id: string) {
|
||||
const memory = await this.repository.get(id);
|
||||
if (!memory) {
|
||||
throw new BadRequestException('Memory not found');
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
}
|
||||
30
server/test/fixtures/memory.stub.ts
vendored
Normal file
30
server/test/fixtures/memory.stub.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MemoryEntity, MemoryType } from 'src/entities/memory.entity';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
export const memoryStub = {
|
||||
empty: <MemoryEntity>{
|
||||
id: 'memoryEmpty',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
memoryAt: new Date(2024),
|
||||
ownerId: userStub.admin.id,
|
||||
owner: userStub.admin,
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 },
|
||||
isSaved: false,
|
||||
assets: [],
|
||||
},
|
||||
memory1: <MemoryEntity>{
|
||||
id: 'memory1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
memoryAt: new Date(2024),
|
||||
ownerId: userStub.admin.id,
|
||||
owner: userStub.admin,
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 },
|
||||
isSaved: false,
|
||||
assets: [assetStub.image1],
|
||||
},
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ export interface IAccessRepositoryMock {
|
|||
authDevice: jest.Mocked<IAccessRepository['authDevice']>;
|
||||
library: jest.Mocked<IAccessRepository['library']>;
|
||||
timeline: jest.Mocked<IAccessRepository['timeline']>;
|
||||
memory: jest.Mocked<IAccessRepository['memory']>;
|
||||
person: jest.Mocked<IAccessRepository['person']>;
|
||||
partner: jest.Mocked<IAccessRepository['partner']>;
|
||||
}
|
||||
|
|
@ -49,6 +50,10 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
|||
checkPartnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
memory: {
|
||||
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
person: {
|
||||
checkFaceOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||
|
|
|
|||
14
server/test/repositories/memory.repository.mock.ts
Normal file
14
server/test/repositories/memory.repository.mock.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||
|
||||
export const newMemoryRepositoryMock = (): jest.Mocked<IMemoryRepository> => {
|
||||
return {
|
||||
search: jest.fn().mockResolvedValue([]),
|
||||
get: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getAssetIds: jest.fn().mockResolvedValue(new Set()),
|
||||
addAssetIds: jest.fn(),
|
||||
removeAssetIds: jest.fn(),
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue