refactor: migrate memory repository (#15532)

This commit is contained in:
Jason Rasmussen 2025-01-22 16:39:13 -05:00 committed by GitHub
parent ca3619658b
commit 1f19a65d1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 47 additions and 55 deletions

View File

@ -93,8 +93,6 @@ describe('/memories', () => {
data: { year: 2021 }, data: { year: 2021 },
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
deletedAt: null,
seenAt: null,
isSaved: false, isSaved: false,
memoryAt: expect.any(String), memoryAt: expect.any(String),
ownerId: user.userId, ownerId: user.userId,

View File

@ -2,8 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { MemoryEntity } from 'src/entities/memory.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { MemoryType } from 'src/enum'; import { MemoryType } from 'src/enum';
import { MemoryItem } from 'src/types';
import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
class MemoryBaseDto { class MemoryBaseDto {
@ -69,18 +70,18 @@ export class MemoryResponseDto {
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
} }
export const mapMemory = (entity: MemoryEntity): MemoryResponseDto => { export const mapMemory = (entity: MemoryItem): MemoryResponseDto => {
return { return {
id: entity.id, id: entity.id,
createdAt: entity.createdAt, createdAt: entity.createdAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
deletedAt: entity.deletedAt, deletedAt: entity.deletedAt ?? undefined,
memoryAt: entity.memoryAt, memoryAt: entity.memoryAt,
seenAt: entity.seenAt, seenAt: entity.seenAt ?? undefined,
ownerId: entity.ownerId, ownerId: entity.ownerId,
type: entity.type, type: entity.type as MemoryType,
data: entity.data, data: entity.data as unknown as MemoryData,
isSaved: entity.isSaved, isSaved: entity.isSaved,
assets: entity.assets.map((asset) => mapAsset(asset)), assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset as AssetEntity)),
}; };
}; };

View File

@ -1,17 +0,0 @@
import { Insertable, Updateable } from 'kysely';
import { Memories } from 'src/db';
import { MemoryEntity, OnThisDayData } from 'src/entities/memory.entity';
import { IBulkAsset } from 'src/utils/asset.util';
export const IMemoryRepository = 'IMemoryRepository';
export interface IMemoryRepository extends IBulkAsset {
search(ownerId: string): Promise<MemoryEntity[]>;
get(id: string): Promise<MemoryEntity | undefined>;
create(
memory: Omit<Insertable<Memories>, 'data'> & { data: OnThisDayData },
assetIds: Set<string>,
): Promise<MemoryEntity>;
update(id: string, memory: Updateable<Memories>): Promise<MemoryEntity>;
delete(id: string): Promise<void>;
}

View File

@ -11,7 +11,6 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface'; import { IMapRepository } from 'src/interfaces/map.interface';
import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface'; import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface'; import { INotificationRepository } from 'src/interfaces/notification.interface';
@ -78,6 +77,7 @@ export const repositories = [
AuditRepository, AuditRepository,
ApiKeyRepository, ApiKeyRepository,
ConfigRepository, ConfigRepository,
MemoryRepository,
ViewRepository, ViewRepository,
]; ];
@ -95,7 +95,6 @@ export const providers = [
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMapRepository, useClass: MapRepository }, { provide: IMapRepository, useClass: MapRepository },
{ provide: IMediaRepository, useClass: MediaRepository }, { provide: IMediaRepository, useClass: MediaRepository },
{ provide: IMemoryRepository, useClass: MemoryRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository }, { provide: IMoveRepository, useClass: MoveRepository },
{ provide: INotificationRepository, useClass: NotificationRepository }, { provide: INotificationRepository, useClass: NotificationRepository },

View File

@ -4,29 +4,28 @@ import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB, Memories } from 'src/db'; import { DB, Memories } from 'src/db';
import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { MemoryEntity } from 'src/entities/memory.entity'; import { IBulkAsset } from 'src/utils/asset.util';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
@Injectable() @Injectable()
export class MemoryRepository implements IMemoryRepository { export class MemoryRepository implements IBulkAsset {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
search(ownerId: string): Promise<MemoryEntity[]> { search(ownerId: string) {
return this.db return this.db
.selectFrom('memories') .selectFrom('memories')
.selectAll() .selectAll()
.where('ownerId', '=', ownerId) .where('ownerId', '=', ownerId)
.orderBy('memoryAt', 'desc') .orderBy('memoryAt', 'desc')
.execute() as Promise<MemoryEntity[]>; .execute();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
get(id: string): Promise<MemoryEntity | undefined> { get(id: string) {
return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise<MemoryEntity | undefined>; return this.getByIdBuilder(id).executeTakeFirst();
} }
async create(memory: Insertable<Memories>, assetIds: Set<string>): Promise<MemoryEntity> { async create(memory: Insertable<Memories>, assetIds: Set<string>) {
const id = await this.db.transaction().execute(async (tx) => { const id = await this.db.transaction().execute(async (tx) => {
const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow(); const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow();
@ -38,25 +37,25 @@ export class MemoryRepository implements IMemoryRepository {
return id; return id;
}); });
return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise<MemoryEntity>; return this.getByIdBuilder(id).executeTakeFirstOrThrow();
} }
@GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] })
async update(id: string, memory: Updateable<Memories>): Promise<MemoryEntity> { async update(id: string, memory: Updateable<Memories>) {
await this.db.updateTable('memories').set(memory).where('id', '=', id).execute(); await this.db.updateTable('memories').set(memory).where('id', '=', id).execute();
return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise<MemoryEntity>; return this.getByIdBuilder(id).executeTakeFirstOrThrow();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> { async delete(id: string) {
await this.db.deleteFrom('memories').where('id', '=', id).execute(); await this.db.deleteFrom('memories').where('id', '=', id).execute();
} }
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@ChunkedSet({ paramIndex: 1 }) @ChunkedSet({ paramIndex: 1 })
async getAssetIds(id: string, assetIds: string[]): Promise<Set<string>> { async getAssetIds(id: string, assetIds: string[]) {
if (assetIds.length === 0) { if (assetIds.length === 0) {
return new Set(); return new Set<string>();
} }
const results = await this.db const results = await this.db
@ -70,7 +69,7 @@ export class MemoryRepository implements IMemoryRepository {
} }
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
async addAssetIds(id: string, assetIds: string[]): Promise<void> { async addAssetIds(id: string, assetIds: string[]) {
if (assetIds.length === 0) { if (assetIds.length === 0) {
return; return;
} }
@ -83,7 +82,7 @@ export class MemoryRepository implements IMemoryRepository {
@Chunked({ paramIndex: 1 }) @Chunked({ paramIndex: 1 })
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
async removeAssetIds(id: string, assetIds: string[]): Promise<void> { async removeAssetIds(id: string, assetIds: string[]) {
if (assetIds.length === 0) { if (assetIds.length === 0) {
return; return;
} }

View File

@ -19,7 +19,6 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface'; import { IMapRepository } from 'src/interfaces/map.interface';
import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface'; import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface'; import { INotificationRepository } from 'src/interfaces/notification.interface';
@ -44,6 +43,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository'; import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config'; import { getConfig, updateConfig } from 'src/utils/config';
@ -70,7 +70,7 @@ export class BaseService {
@Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository, @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository,
@Inject(IMapRepository) protected mapRepository: IMapRepository, @Inject(IMapRepository) protected mapRepository: IMapRepository,
@Inject(IMediaRepository) protected mediaRepository: IMediaRepository, @Inject(IMediaRepository) protected mediaRepository: IMediaRepository,
@Inject(IMemoryRepository) protected memoryRepository: IMemoryRepository, protected memoryRepository: MemoryRepository,
@Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository, @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository,
@Inject(IMoveRepository) protected moveRepository: IMoveRepository, @Inject(IMoveRepository) protected moveRepository: IMoveRepository,
@Inject(INotificationRepository) protected notificationRepository: INotificationRepository, @Inject(INotificationRepository) protected notificationRepository: INotificationRepository,

View File

@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { MemoryType } from 'src/enum'; import { MemoryType } from 'src/enum';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { MemoryService } from 'src/services/memory.service'; import { MemoryService } from 'src/services/memory.service';
import { IMemoryRepository } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { memoryStub } from 'test/fixtures/memory.stub'; import { memoryStub } from 'test/fixtures/memory.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';

View File

@ -1,4 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { JsonObject } from 'src/db';
import { BulkIdResponseDto, BulkIdsDto } 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 { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
@ -32,7 +33,7 @@ export class MemoryService extends BaseService {
{ {
ownerId: auth.user.id, ownerId: auth.user.id,
type: dto.type, type: dto.type,
data: dto.data, data: dto.data as unknown as JsonObject,
isSaved: dto.isSaved, isSaved: dto.isSaved,
memoryAt: dto.memoryAt, memoryAt: dto.memoryAt,
seenAt: dto.seenAt, seenAt: dto.seenAt,

View File

@ -5,6 +5,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository'; import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
@ -23,6 +24,7 @@ export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInter
export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>; export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>;
export type IAuditRepository = RepositoryInterface<AuditRepository>; export type IAuditRepository = RepositoryInterface<AuditRepository>;
export type IConfigRepository = RepositoryInterface<ConfigRepository>; export type IConfigRepository = RepositoryInterface<ConfigRepository>;
export type IMemoryRepository = RepositoryInterface<MemoryRepository>;
export type IViewRepository = RepositoryInterface<ViewRepository>; export type IViewRepository = RepositoryInterface<ViewRepository>;
export type ActivityItem = export type ActivityItem =
@ -33,3 +35,7 @@ export type ApiKeyItem =
| Awaited<ReturnType<IApiKeyRepository['create']>> | Awaited<ReturnType<IApiKeyRepository['create']>>
| NonNullable<Awaited<ReturnType<IApiKeyRepository['getById']>>> | NonNullable<Awaited<ReturnType<IApiKeyRepository['getById']>>>
| Awaited<ReturnType<IApiKeyRepository['getByUserId']>>[0]; | Awaited<ReturnType<IApiKeyRepository['getByUserId']>>[0];
export type MemoryItem =
| Awaited<ReturnType<IMemoryRepository['create']>>
| Awaited<ReturnType<IMemoryRepository['search']>>[0];

View File

@ -1,10 +1,9 @@
import { MemoryEntity } from 'src/entities/memory.entity';
import { MemoryType } from 'src/enum'; import { MemoryType } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
export const memoryStub = { export const memoryStub = {
empty: <MemoryEntity>{ empty: {
id: 'memoryEmpty', id: 'memoryEmpty',
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
@ -15,8 +14,10 @@ export const memoryStub = {
data: { year: 2024 }, data: { year: 2024 },
isSaved: false, isSaved: false,
assets: [], assets: [],
}, deletedAt: null,
memory1: <MemoryEntity>{ seenAt: null,
} as unknown as any,
memory1: {
id: 'memory1', id: 'memory1',
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
@ -27,5 +28,7 @@ export const memoryStub = {
data: { year: 2024 }, data: { year: 2024 },
isSaved: false, isSaved: false,
assets: [assetStub.image1], assets: [assetStub.image1],
}, deletedAt: null,
seenAt: null,
} as unknown as any,
}; };

View File

@ -1,4 +1,4 @@
import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { IMemoryRepository } from 'src/types';
import { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
export const newMemoryRepositoryMock = (): Mocked<IMemoryRepository> => { export const newMemoryRepositoryMock = (): Mocked<IMemoryRepository> => {

View File

@ -7,6 +7,7 @@ import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository'; import { ActivityRepository } from 'src/repositories/activity.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository'; import { AuditRepository } from 'src/repositories/audit.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { import {
@ -14,6 +15,7 @@ import {
IActivityRepository, IActivityRepository,
IApiKeyRepository, IApiKeyRepository,
IAuditRepository, IAuditRepository,
IMemoryRepository,
IViewRepository, IViewRepository,
} from 'src/types'; } from 'src/types';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@ -132,7 +134,7 @@ export const newTestService = <T extends BaseService>(
machineLearningMock, machineLearningMock,
mapMock, mapMock,
mediaMock, mediaMock,
memoryMock, memoryMock as IMemoryRepository as MemoryRepository,
metadataMock, metadataMock,
moveMock, moveMock,
notificationMock, notificationMock,